agmem 0.2.1__py3-none-any.whl → 0.3.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.
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/METADATA +338 -27
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/RECORD +21 -9
- memvcs/core/agents.py +411 -0
- memvcs/core/archaeology.py +410 -0
- memvcs/core/collaboration.py +435 -0
- memvcs/core/compliance.py +427 -0
- memvcs/core/confidence.py +379 -0
- memvcs/core/daemon.py +735 -0
- memvcs/core/delta.py +45 -23
- memvcs/core/private_search.py +327 -0
- memvcs/core/search_index.py +538 -0
- memvcs/core/semantic_graph.py +388 -0
- memvcs/core/session.py +520 -0
- memvcs/core/timetravel.py +430 -0
- memvcs/integrations/mcp_server.py +775 -4
- memvcs/integrations/web_ui/server.py +424 -0
- memvcs/integrations/web_ui/websocket.py +223 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/WHEEL +0 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/entry_points.txt +0 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-Agent Collaboration - Trust management and agent registry.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- Agent identity and key management
|
|
6
|
+
- Trust relationships between agents
|
|
7
|
+
- Contribution tracking and attribution
|
|
8
|
+
- Conflict detection and resolution helpers
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import uuid
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Agent:
|
|
23
|
+
"""Represents an agent identity."""
|
|
24
|
+
|
|
25
|
+
agent_id: str
|
|
26
|
+
name: str
|
|
27
|
+
public_key: Optional[str] = None
|
|
28
|
+
created_at: Optional[str] = None
|
|
29
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"agent_id": self.agent_id,
|
|
34
|
+
"name": self.name,
|
|
35
|
+
"public_key": self.public_key,
|
|
36
|
+
"created_at": self.created_at,
|
|
37
|
+
"metadata": self.metadata,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Agent":
|
|
42
|
+
return cls(
|
|
43
|
+
agent_id=data["agent_id"],
|
|
44
|
+
name=data["name"],
|
|
45
|
+
public_key=data.get("public_key"),
|
|
46
|
+
created_at=data.get("created_at"),
|
|
47
|
+
metadata=data.get("metadata", {}),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class TrustRelation:
|
|
53
|
+
"""A trust relationship between two agents."""
|
|
54
|
+
|
|
55
|
+
from_agent: str
|
|
56
|
+
to_agent: str
|
|
57
|
+
trust_level: str # "full", "partial", "read-only", "none"
|
|
58
|
+
created_at: str
|
|
59
|
+
reason: Optional[str] = None
|
|
60
|
+
expires_at: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"from_agent": self.from_agent,
|
|
65
|
+
"to_agent": self.to_agent,
|
|
66
|
+
"trust_level": self.trust_level,
|
|
67
|
+
"created_at": self.created_at,
|
|
68
|
+
"reason": self.reason,
|
|
69
|
+
"expires_at": self.expires_at,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TrustRelation":
|
|
74
|
+
return cls(
|
|
75
|
+
from_agent=data["from_agent"],
|
|
76
|
+
to_agent=data["to_agent"],
|
|
77
|
+
trust_level=data["trust_level"],
|
|
78
|
+
created_at=data["created_at"],
|
|
79
|
+
reason=data.get("reason"),
|
|
80
|
+
expires_at=data.get("expires_at"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Contribution:
|
|
86
|
+
"""A contribution by an agent to memory."""
|
|
87
|
+
|
|
88
|
+
agent_id: str
|
|
89
|
+
commit_hash: str
|
|
90
|
+
timestamp: str
|
|
91
|
+
files_changed: int
|
|
92
|
+
additions: int
|
|
93
|
+
deletions: int
|
|
94
|
+
message: Optional[str] = None
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"agent_id": self.agent_id,
|
|
99
|
+
"commit_hash": self.commit_hash,
|
|
100
|
+
"timestamp": self.timestamp,
|
|
101
|
+
"files_changed": self.files_changed,
|
|
102
|
+
"additions": self.additions,
|
|
103
|
+
"deletions": self.deletions,
|
|
104
|
+
"message": self.message,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AgentRegistry:
|
|
109
|
+
"""Registry for managing agent identities."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, mem_dir: Path):
|
|
112
|
+
self.mem_dir = Path(mem_dir)
|
|
113
|
+
self.agents_file = self.mem_dir / "agents.json"
|
|
114
|
+
self._agents: Dict[str, Agent] = {}
|
|
115
|
+
self._load()
|
|
116
|
+
|
|
117
|
+
def _load(self) -> None:
|
|
118
|
+
"""Load agents from disk."""
|
|
119
|
+
if self.agents_file.exists():
|
|
120
|
+
try:
|
|
121
|
+
data = json.loads(self.agents_file.read_text())
|
|
122
|
+
self._agents = {
|
|
123
|
+
aid: Agent.from_dict(a) for aid, a in data.get("agents", {}).items()
|
|
124
|
+
}
|
|
125
|
+
except Exception:
|
|
126
|
+
self._agents = {}
|
|
127
|
+
|
|
128
|
+
def _save(self) -> None:
|
|
129
|
+
"""Save agents to disk."""
|
|
130
|
+
self.mem_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
data = {"agents": {aid: a.to_dict() for aid, a in self._agents.items()}}
|
|
132
|
+
self.agents_file.write_text(json.dumps(data, indent=2))
|
|
133
|
+
|
|
134
|
+
def register_agent(
|
|
135
|
+
self, name: str, public_key: Optional[str] = None, metadata: Optional[Dict] = None
|
|
136
|
+
) -> Agent:
|
|
137
|
+
"""Register a new agent."""
|
|
138
|
+
agent_id = hashlib.sha256(
|
|
139
|
+
f"{name}{datetime.now(timezone.utc).isoformat()}".encode()
|
|
140
|
+
).hexdigest()[:16]
|
|
141
|
+
|
|
142
|
+
agent = Agent(
|
|
143
|
+
agent_id=agent_id,
|
|
144
|
+
name=name,
|
|
145
|
+
public_key=public_key,
|
|
146
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
147
|
+
metadata=metadata or {},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self._agents[agent_id] = agent
|
|
151
|
+
self._save()
|
|
152
|
+
return agent
|
|
153
|
+
|
|
154
|
+
def get_agent(self, agent_id: str) -> Optional[Agent]:
|
|
155
|
+
"""Get an agent by ID."""
|
|
156
|
+
return self._agents.get(agent_id)
|
|
157
|
+
|
|
158
|
+
def list_agents(self) -> List[Agent]:
|
|
159
|
+
"""List all registered agents."""
|
|
160
|
+
return list(self._agents.values())
|
|
161
|
+
|
|
162
|
+
def remove_agent(self, agent_id: str) -> bool:
|
|
163
|
+
"""Remove an agent."""
|
|
164
|
+
if agent_id in self._agents:
|
|
165
|
+
del self._agents[agent_id]
|
|
166
|
+
self._save()
|
|
167
|
+
return True
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TrustManager:
|
|
172
|
+
"""Manages trust relationships between agents."""
|
|
173
|
+
|
|
174
|
+
TRUST_LEVELS = ["full", "partial", "read-only", "none"]
|
|
175
|
+
|
|
176
|
+
def __init__(self, mem_dir: Path):
|
|
177
|
+
self.mem_dir = Path(mem_dir)
|
|
178
|
+
self.trust_file = self.mem_dir / "trust.json"
|
|
179
|
+
self._relations: List[TrustRelation] = []
|
|
180
|
+
self._load()
|
|
181
|
+
|
|
182
|
+
def _load(self) -> None:
|
|
183
|
+
"""Load trust relations from disk."""
|
|
184
|
+
if self.trust_file.exists():
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(self.trust_file.read_text())
|
|
187
|
+
self._relations = [TrustRelation.from_dict(r) for r in data.get("relations", [])]
|
|
188
|
+
except Exception:
|
|
189
|
+
self._relations = []
|
|
190
|
+
|
|
191
|
+
def _save(self) -> None:
|
|
192
|
+
"""Save trust relations to disk."""
|
|
193
|
+
self.mem_dir.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
data = {"relations": [r.to_dict() for r in self._relations]}
|
|
195
|
+
self.trust_file.write_text(json.dumps(data, indent=2))
|
|
196
|
+
|
|
197
|
+
def grant_trust(
|
|
198
|
+
self,
|
|
199
|
+
from_agent: str,
|
|
200
|
+
to_agent: str,
|
|
201
|
+
trust_level: str,
|
|
202
|
+
reason: Optional[str] = None,
|
|
203
|
+
expires_at: Optional[str] = None,
|
|
204
|
+
) -> TrustRelation:
|
|
205
|
+
"""Grant trust from one agent to another."""
|
|
206
|
+
if trust_level not in self.TRUST_LEVELS:
|
|
207
|
+
raise ValueError(f"Invalid trust level: {trust_level}")
|
|
208
|
+
|
|
209
|
+
# Remove existing relation
|
|
210
|
+
self._relations = [
|
|
211
|
+
r
|
|
212
|
+
for r in self._relations
|
|
213
|
+
if not (r.from_agent == from_agent and r.to_agent == to_agent)
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
relation = TrustRelation(
|
|
217
|
+
from_agent=from_agent,
|
|
218
|
+
to_agent=to_agent,
|
|
219
|
+
trust_level=trust_level,
|
|
220
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
221
|
+
reason=reason,
|
|
222
|
+
expires_at=expires_at,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
self._relations.append(relation)
|
|
226
|
+
self._save()
|
|
227
|
+
return relation
|
|
228
|
+
|
|
229
|
+
def revoke_trust(self, from_agent: str, to_agent: str) -> bool:
|
|
230
|
+
"""Revoke trust between agents."""
|
|
231
|
+
original_count = len(self._relations)
|
|
232
|
+
self._relations = [
|
|
233
|
+
r
|
|
234
|
+
for r in self._relations
|
|
235
|
+
if not (r.from_agent == from_agent and r.to_agent == to_agent)
|
|
236
|
+
]
|
|
237
|
+
if len(self._relations) < original_count:
|
|
238
|
+
self._save()
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
def get_trust_level(self, from_agent: str, to_agent: str) -> str:
|
|
243
|
+
"""Get trust level from one agent to another."""
|
|
244
|
+
for r in self._relations:
|
|
245
|
+
if r.from_agent == from_agent and r.to_agent == to_agent:
|
|
246
|
+
return r.trust_level
|
|
247
|
+
return "none"
|
|
248
|
+
|
|
249
|
+
def get_trusted_by(self, agent_id: str) -> List[TrustRelation]:
|
|
250
|
+
"""Get agents that trust this agent."""
|
|
251
|
+
return [r for r in self._relations if r.to_agent == agent_id]
|
|
252
|
+
|
|
253
|
+
def get_trusts(self, agent_id: str) -> List[TrustRelation]:
|
|
254
|
+
"""Get agents this agent trusts."""
|
|
255
|
+
return [r for r in self._relations if r.from_agent == agent_id]
|
|
256
|
+
|
|
257
|
+
def get_trust_graph(self) -> Dict[str, Any]:
|
|
258
|
+
"""Get trust graph data for visualization."""
|
|
259
|
+
agents: Set[str] = set()
|
|
260
|
+
for r in self._relations:
|
|
261
|
+
agents.add(r.from_agent)
|
|
262
|
+
agents.add(r.to_agent)
|
|
263
|
+
|
|
264
|
+
nodes = [{"id": a, "name": a[:8]} for a in agents]
|
|
265
|
+
links = [
|
|
266
|
+
{
|
|
267
|
+
"source": r.from_agent,
|
|
268
|
+
"target": r.to_agent,
|
|
269
|
+
"trust_level": r.trust_level,
|
|
270
|
+
"value": {"full": 3, "partial": 2, "read-only": 1, "none": 0}.get(r.trust_level, 0),
|
|
271
|
+
}
|
|
272
|
+
for r in self._relations
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
return {"nodes": nodes, "links": links}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class ContributionTracker:
|
|
279
|
+
"""Tracks contributions by agents to memory."""
|
|
280
|
+
|
|
281
|
+
def __init__(self, mem_dir: Path):
|
|
282
|
+
self.mem_dir = Path(mem_dir)
|
|
283
|
+
self.contributions_file = self.mem_dir / "contributions.json"
|
|
284
|
+
self._contributions: List[Contribution] = []
|
|
285
|
+
self._load()
|
|
286
|
+
|
|
287
|
+
def _load(self) -> None:
|
|
288
|
+
"""Load contributions from disk."""
|
|
289
|
+
if self.contributions_file.exists():
|
|
290
|
+
try:
|
|
291
|
+
data = json.loads(self.contributions_file.read_text())
|
|
292
|
+
self._contributions = [Contribution(**c) for c in data.get("contributions", [])]
|
|
293
|
+
except Exception:
|
|
294
|
+
self._contributions = []
|
|
295
|
+
|
|
296
|
+
def _save(self) -> None:
|
|
297
|
+
"""Save contributions to disk."""
|
|
298
|
+
self.mem_dir.mkdir(parents=True, exist_ok=True)
|
|
299
|
+
data = {"contributions": [c.to_dict() for c in self._contributions]}
|
|
300
|
+
self.contributions_file.write_text(json.dumps(data, indent=2))
|
|
301
|
+
|
|
302
|
+
def record_contribution(
|
|
303
|
+
self,
|
|
304
|
+
agent_id: str,
|
|
305
|
+
commit_hash: str,
|
|
306
|
+
files_changed: int,
|
|
307
|
+
additions: int,
|
|
308
|
+
deletions: int,
|
|
309
|
+
message: Optional[str] = None,
|
|
310
|
+
) -> Contribution:
|
|
311
|
+
"""Record a contribution by an agent."""
|
|
312
|
+
contribution = Contribution(
|
|
313
|
+
agent_id=agent_id,
|
|
314
|
+
commit_hash=commit_hash,
|
|
315
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
316
|
+
files_changed=files_changed,
|
|
317
|
+
additions=additions,
|
|
318
|
+
deletions=deletions,
|
|
319
|
+
message=message,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
self._contributions.append(contribution)
|
|
323
|
+
self._save()
|
|
324
|
+
return contribution
|
|
325
|
+
|
|
326
|
+
def get_contributions(self, agent_id: str) -> List[Contribution]:
|
|
327
|
+
"""Get all contributions by an agent."""
|
|
328
|
+
return [c for c in self._contributions if c.agent_id == agent_id]
|
|
329
|
+
|
|
330
|
+
def get_leaderboard(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
331
|
+
"""Get leaderboard of top contributors."""
|
|
332
|
+
stats: Dict[str, Dict[str, int]] = {}
|
|
333
|
+
|
|
334
|
+
for c in self._contributions:
|
|
335
|
+
if c.agent_id not in stats:
|
|
336
|
+
stats[c.agent_id] = {"commits": 0, "additions": 0, "deletions": 0}
|
|
337
|
+
stats[c.agent_id]["commits"] += 1
|
|
338
|
+
stats[c.agent_id]["additions"] += c.additions
|
|
339
|
+
stats[c.agent_id]["deletions"] += c.deletions
|
|
340
|
+
|
|
341
|
+
sorted_agents = sorted(
|
|
342
|
+
stats.items(),
|
|
343
|
+
key=lambda x: x[1]["commits"],
|
|
344
|
+
reverse=True,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return [
|
|
348
|
+
{"agent_id": aid, "rank": i + 1, **s}
|
|
349
|
+
for i, (aid, s) in enumerate(sorted_agents[:limit])
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
def get_timeline(self, limit: int = 20) -> List[Contribution]:
|
|
353
|
+
"""Get recent contributions timeline."""
|
|
354
|
+
sorted_contributions = sorted(
|
|
355
|
+
self._contributions,
|
|
356
|
+
key=lambda c: c.timestamp,
|
|
357
|
+
reverse=True,
|
|
358
|
+
)
|
|
359
|
+
return sorted_contributions[:limit]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class ConflictDetector:
|
|
363
|
+
"""Detects and helps resolve conflicts between agents."""
|
|
364
|
+
|
|
365
|
+
def __init__(self, repo_root: Path):
|
|
366
|
+
self.repo_root = Path(repo_root)
|
|
367
|
+
|
|
368
|
+
def detect_conflicts(self, base_commit: str, head_commits: List[str]) -> List[Dict[str, Any]]:
|
|
369
|
+
"""Detect conflicts between commits from different agents."""
|
|
370
|
+
conflicts = []
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
from memvcs.core.repository import Repository
|
|
374
|
+
from memvcs.core.diff import DiffEngine
|
|
375
|
+
|
|
376
|
+
repo = Repository(self.repo_root)
|
|
377
|
+
engine = DiffEngine(repo.object_store)
|
|
378
|
+
|
|
379
|
+
# Get files changed in each head commit
|
|
380
|
+
head_files: Dict[str, Set[str]] = {}
|
|
381
|
+
for head in head_commits:
|
|
382
|
+
diff = engine.diff_commits(base_commit, head)
|
|
383
|
+
head_files[head] = set(f.path for f in diff.files)
|
|
384
|
+
|
|
385
|
+
# Find overlapping files
|
|
386
|
+
all_heads = list(head_files.keys())
|
|
387
|
+
for i, head1 in enumerate(all_heads):
|
|
388
|
+
for head2 in all_heads[i + 1 :]:
|
|
389
|
+
overlapping = head_files[head1] & head_files[head2]
|
|
390
|
+
for path in overlapping:
|
|
391
|
+
conflicts.append(
|
|
392
|
+
{
|
|
393
|
+
"path": path,
|
|
394
|
+
"commits": [head1, head2],
|
|
395
|
+
"type": "concurrent_modification",
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
except Exception:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
return conflicts
|
|
403
|
+
|
|
404
|
+
def suggest_resolution(self, conflict: Dict[str, Any]) -> Dict[str, Any]:
|
|
405
|
+
"""Suggest a resolution for a conflict."""
|
|
406
|
+
suggestions = {
|
|
407
|
+
"concurrent_modification": [
|
|
408
|
+
"Use the most recent version",
|
|
409
|
+
"Merge changes manually",
|
|
410
|
+
"Ask the agents to resolve",
|
|
411
|
+
],
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
conflict_type = conflict.get("type", "unknown")
|
|
415
|
+
return {
|
|
416
|
+
"conflict": conflict,
|
|
417
|
+
"suggestions": suggestions.get(conflict_type, ["Manual review required"]),
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# --- Web UI Helpers ---
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def get_collaboration_dashboard(mem_dir: Path) -> Dict[str, Any]:
|
|
425
|
+
"""Get data for the collaboration dashboard."""
|
|
426
|
+
registry = AgentRegistry(mem_dir)
|
|
427
|
+
trust_mgr = TrustManager(mem_dir)
|
|
428
|
+
contrib_tracker = ContributionTracker(mem_dir)
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
"agents": [a.to_dict() for a in registry.list_agents()],
|
|
432
|
+
"trust_graph": trust_mgr.get_trust_graph(),
|
|
433
|
+
"leaderboard": contrib_tracker.get_leaderboard(),
|
|
434
|
+
"recent_activity": [c.to_dict() for c in contrib_tracker.get_timeline(10)],
|
|
435
|
+
}
|