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
|
@@ -25,6 +25,18 @@ logging.basicConfig(
|
|
|
25
25
|
logger = logging.getLogger("agmem-mcp")
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _capture_observation(tool_name: str, arguments: dict, result: str) -> None:
|
|
29
|
+
"""Capture tool call as observation if daemon is running."""
|
|
30
|
+
try:
|
|
31
|
+
from memvcs.core.daemon import capture_observation
|
|
32
|
+
|
|
33
|
+
capture_observation(tool_name, arguments, result)
|
|
34
|
+
except ImportError:
|
|
35
|
+
pass # Daemon module not available
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.debug(f"Observation capture failed: {e}")
|
|
38
|
+
|
|
39
|
+
|
|
28
40
|
def _get_repo():
|
|
29
41
|
"""Get repository from cwd. Returns (repo, None) or (None, error_msg)."""
|
|
30
42
|
from memvcs.core.repository import Repository
|
|
@@ -129,8 +141,11 @@ def _create_mcp_server():
|
|
|
129
141
|
pass
|
|
130
142
|
|
|
131
143
|
if not results:
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
result = f"No matches for '{query}' in memory."
|
|
145
|
+
else:
|
|
146
|
+
result = "\n\n".join(results[:10])
|
|
147
|
+
_capture_observation("memory_search", {"query": query, "memory_type": memory_type}, result)
|
|
148
|
+
return result
|
|
134
149
|
|
|
135
150
|
@mcp.tool()
|
|
136
151
|
def memory_add(path: str, commit: bool = False, message: str = "") -> str:
|
|
@@ -164,10 +179,14 @@ def _create_mcp_server():
|
|
|
164
179
|
return "Staged. Error: message required for commit."
|
|
165
180
|
try:
|
|
166
181
|
commit_hash = repo.commit(message)
|
|
167
|
-
|
|
182
|
+
result = f"Staged and committed: {rel_path} ({commit_hash[:8]})"
|
|
183
|
+
_capture_observation("memory_add", {"path": path, "commit": True}, result)
|
|
184
|
+
return result
|
|
168
185
|
except Exception as e:
|
|
169
186
|
return f"Staged. Error committing: {e}"
|
|
170
|
-
|
|
187
|
+
result = f"Staged: {rel_path}. Run 'agmem commit -m \"message\"' to save."
|
|
188
|
+
_capture_observation("memory_add", {"path": path, "commit": False}, result)
|
|
189
|
+
return result
|
|
171
190
|
|
|
172
191
|
@mcp.tool()
|
|
173
192
|
def memory_log(max_count: int = 10) -> str:
|
|
@@ -251,6 +270,758 @@ def _create_mcp_server():
|
|
|
251
270
|
|
|
252
271
|
return full_path.read_text(encoding="utf-8", errors="replace")
|
|
253
272
|
|
|
273
|
+
# --- Progressive Disclosure Search Tools ---
|
|
274
|
+
|
|
275
|
+
@mcp.tool()
|
|
276
|
+
def memory_index(query: str, memory_type: Optional[str] = None, limit: int = 20) -> str:
|
|
277
|
+
"""Layer 1: Lightweight search returning metadata + first line only.
|
|
278
|
+
|
|
279
|
+
Use this first to find relevant memories with minimal token cost.
|
|
280
|
+
Follow up with memory_details for full content of specific files.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
query: Search query
|
|
284
|
+
memory_type: Filter by type (episodic, semantic, procedural)
|
|
285
|
+
limit: Maximum results (default 20)
|
|
286
|
+
"""
|
|
287
|
+
repo, err = _get_repo()
|
|
288
|
+
if err:
|
|
289
|
+
return f"Error: {err}"
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
from memvcs.core.search_index import SearchIndex, layer1_cost
|
|
293
|
+
|
|
294
|
+
index = SearchIndex(repo.mem_dir)
|
|
295
|
+
# Ensure index is up to date
|
|
296
|
+
index.index_directory(repo.current_dir)
|
|
297
|
+
|
|
298
|
+
results = index.search_index(query, memory_type=memory_type, limit=limit)
|
|
299
|
+
index.close()
|
|
300
|
+
|
|
301
|
+
if not results:
|
|
302
|
+
return f"No results for: {query}"
|
|
303
|
+
|
|
304
|
+
lines = [f"Found {len(results)} results (est. ~{layer1_cost(results)} tokens):"]
|
|
305
|
+
for r in results:
|
|
306
|
+
lines.append(f"- [{r.memory_type}] {r.filename}: {r.first_line[:80]}...")
|
|
307
|
+
lines.append(f" Path: {r.path}")
|
|
308
|
+
return "\n".join(lines)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
return f"Error: {e}"
|
|
311
|
+
|
|
312
|
+
@mcp.tool()
|
|
313
|
+
def memory_timeline(days: int = 7, limit: int = 10) -> str:
|
|
314
|
+
"""Layer 2: Get timeline of memory activity grouped by date.
|
|
315
|
+
|
|
316
|
+
Shows what was captured each day without full content.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
days: Number of days to look back (default 7)
|
|
320
|
+
limit: Maximum entries per day (default 10)
|
|
321
|
+
"""
|
|
322
|
+
repo, err = _get_repo()
|
|
323
|
+
if err:
|
|
324
|
+
return f"Error: {err}"
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
from memvcs.core.search_index import SearchIndex
|
|
328
|
+
from datetime import datetime, timedelta, timezone
|
|
329
|
+
|
|
330
|
+
index = SearchIndex(repo.mem_dir)
|
|
331
|
+
index.index_directory(repo.current_dir)
|
|
332
|
+
|
|
333
|
+
start_date = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
334
|
+
timeline = index.get_timeline(start_date=start_date, limit=limit)
|
|
335
|
+
index.close()
|
|
336
|
+
|
|
337
|
+
if not timeline:
|
|
338
|
+
return "No activity in timeline."
|
|
339
|
+
|
|
340
|
+
lines = [f"Memory timeline (last {days} days):"]
|
|
341
|
+
for entry in timeline:
|
|
342
|
+
lines.append(f"\n## {entry.date} ({entry.file_count} files)")
|
|
343
|
+
for f in entry.files[:5]:
|
|
344
|
+
lines.append(f" - [{f['memory_type']}] {f['filename']}")
|
|
345
|
+
if len(entry.files) > 5:
|
|
346
|
+
lines.append(f" ... and {len(entry.files) - 5} more")
|
|
347
|
+
return "\n".join(lines)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
return f"Error: {e}"
|
|
350
|
+
|
|
351
|
+
@mcp.tool()
|
|
352
|
+
def memory_details(paths: str) -> str:
|
|
353
|
+
"""Layer 3: Get full content of specific memory files.
|
|
354
|
+
|
|
355
|
+
Use after memory_index to retrieve complete content.
|
|
356
|
+
Higher token cost - use sparingly.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
paths: Comma-separated list of file paths from memory_index results
|
|
360
|
+
"""
|
|
361
|
+
repo, err = _get_repo()
|
|
362
|
+
if err:
|
|
363
|
+
return f"Error: {err}"
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
from memvcs.core.search_index import SearchIndex, layer3_cost
|
|
367
|
+
|
|
368
|
+
path_list = [p.strip() for p in paths.split(",")]
|
|
369
|
+
|
|
370
|
+
index = SearchIndex(repo.mem_dir)
|
|
371
|
+
details = index.get_full_details(path_list)
|
|
372
|
+
index.close()
|
|
373
|
+
|
|
374
|
+
if not details:
|
|
375
|
+
return "No files found."
|
|
376
|
+
|
|
377
|
+
lines = [f"Retrieved {len(details)} files (est. ~{layer3_cost(details)} tokens):"]
|
|
378
|
+
for d in details:
|
|
379
|
+
lines.append(f"\n---\n## {d['filename']}\n")
|
|
380
|
+
lines.append(d["content"])
|
|
381
|
+
return "\n".join(lines)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
return f"Error: {e}"
|
|
384
|
+
|
|
385
|
+
# --- Session Management Tools ---
|
|
386
|
+
|
|
387
|
+
@mcp.tool()
|
|
388
|
+
def session_start(context: Optional[str] = None) -> str:
|
|
389
|
+
"""Start a new work session for automatic memory capture.
|
|
390
|
+
|
|
391
|
+
Sessions group related observations and create semantic commits.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
context: Optional project or task context description
|
|
395
|
+
"""
|
|
396
|
+
repo, err = _get_repo()
|
|
397
|
+
if err:
|
|
398
|
+
return f"Error: {err}"
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
from memvcs.core.session import SessionManager
|
|
402
|
+
|
|
403
|
+
manager = SessionManager(repo.root)
|
|
404
|
+
session = manager.start_session(project_context=context)
|
|
405
|
+
|
|
406
|
+
return f"Session started: {session.id}\nContext: {context or 'None'}\nStatus: {session.status}"
|
|
407
|
+
except Exception as e:
|
|
408
|
+
return f"Error: {e}"
|
|
409
|
+
|
|
410
|
+
@mcp.tool()
|
|
411
|
+
def session_status() -> str:
|
|
412
|
+
"""Get current session status and statistics."""
|
|
413
|
+
repo, err = _get_repo()
|
|
414
|
+
if err:
|
|
415
|
+
return f"Error: {err}"
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
from memvcs.core.session import SessionManager
|
|
419
|
+
|
|
420
|
+
manager = SessionManager(repo.root)
|
|
421
|
+
status = manager.get_status()
|
|
422
|
+
|
|
423
|
+
if not status.get("active"):
|
|
424
|
+
return "No active session. Use session_start to begin."
|
|
425
|
+
|
|
426
|
+
lines = [
|
|
427
|
+
f"Session: {status['session_id']}",
|
|
428
|
+
f"Status: {status['status']}",
|
|
429
|
+
f"Started: {status['started_at']}",
|
|
430
|
+
f"Observations: {status['observation_count']}",
|
|
431
|
+
f"Topics: {', '.join(status.get('topics', [])) or 'None'}",
|
|
432
|
+
f"Commits: {status['commit_count']}",
|
|
433
|
+
]
|
|
434
|
+
return "\n".join(lines)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
return f"Error: {e}"
|
|
437
|
+
|
|
438
|
+
@mcp.tool()
|
|
439
|
+
def session_commit(end_session: bool = False) -> str:
|
|
440
|
+
"""Commit current session observations to memory.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
end_session: If True, also end the session after committing
|
|
444
|
+
"""
|
|
445
|
+
repo, err = _get_repo()
|
|
446
|
+
if err:
|
|
447
|
+
return f"Error: {err}"
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
from memvcs.core.session import SessionManager
|
|
451
|
+
|
|
452
|
+
manager = SessionManager(repo.root)
|
|
453
|
+
|
|
454
|
+
if not manager.session:
|
|
455
|
+
return "No active session."
|
|
456
|
+
|
|
457
|
+
if end_session:
|
|
458
|
+
commit_hash = manager.end_session(commit=True)
|
|
459
|
+
return f"Session ended and committed: {commit_hash or 'no changes'}"
|
|
460
|
+
else:
|
|
461
|
+
commit_hash = manager._commit_session()
|
|
462
|
+
return f"Session committed: {commit_hash or 'no changes'}"
|
|
463
|
+
except Exception as e:
|
|
464
|
+
return f"Error: {e}"
|
|
465
|
+
|
|
466
|
+
@mcp.tool()
|
|
467
|
+
def session_end(commit: bool = True) -> str:
|
|
468
|
+
"""End the current work session.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
commit: If True, commit observations before ending
|
|
472
|
+
"""
|
|
473
|
+
repo, err = _get_repo()
|
|
474
|
+
if err:
|
|
475
|
+
return f"Error: {err}"
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
from memvcs.core.session import SessionManager
|
|
479
|
+
|
|
480
|
+
manager = SessionManager(repo.root)
|
|
481
|
+
commit_hash = manager.end_session(commit=commit)
|
|
482
|
+
return f"Session ended. Commit: {commit_hash or 'no changes'}"
|
|
483
|
+
except Exception as e:
|
|
484
|
+
return f"Error: {e}"
|
|
485
|
+
|
|
486
|
+
# --- Collaboration Tools ---
|
|
487
|
+
|
|
488
|
+
@mcp.tool()
|
|
489
|
+
def agent_register(name: str, agent_type: str = "assistant") -> str:
|
|
490
|
+
"""Register a new agent in the collaboration registry.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
name: Agent name (e.g., "Claude", "GPT-4")
|
|
494
|
+
agent_type: Type of agent ("assistant", "human", "system")
|
|
495
|
+
"""
|
|
496
|
+
repo, err = _get_repo()
|
|
497
|
+
if err:
|
|
498
|
+
return f"Error: {err}"
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
from memvcs.core.collaboration import AgentRegistry
|
|
502
|
+
|
|
503
|
+
registry = AgentRegistry(repo.mem_dir)
|
|
504
|
+
agent = registry.register_agent(name, metadata={"type": agent_type})
|
|
505
|
+
return f"Agent registered: {agent.agent_id} ({agent.name})"
|
|
506
|
+
except Exception as e:
|
|
507
|
+
return f"Error: {e}"
|
|
508
|
+
|
|
509
|
+
@mcp.tool()
|
|
510
|
+
def trust_grant(from_agent: str, to_agent: str, level: str = "partial") -> str:
|
|
511
|
+
"""Grant trust from one agent to another.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
from_agent: Agent ID granting trust
|
|
515
|
+
to_agent: Agent ID receiving trust
|
|
516
|
+
level: Trust level ("full", "partial", "read-only", "none")
|
|
517
|
+
"""
|
|
518
|
+
repo, err = _get_repo()
|
|
519
|
+
if err:
|
|
520
|
+
return f"Error: {err}"
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
from memvcs.core.collaboration import TrustManager
|
|
524
|
+
|
|
525
|
+
trust_mgr = TrustManager(repo.mem_dir)
|
|
526
|
+
relation = trust_mgr.grant_trust(from_agent, to_agent, level)
|
|
527
|
+
return f"Trust granted: {from_agent} -> {to_agent} ({level})"
|
|
528
|
+
except Exception as e:
|
|
529
|
+
return f"Error: {e}"
|
|
530
|
+
|
|
531
|
+
@mcp.tool()
|
|
532
|
+
def contributions_list(limit: int = 10) -> str:
|
|
533
|
+
"""Get contributor leaderboard.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
limit: Maximum contributors to show
|
|
537
|
+
"""
|
|
538
|
+
repo, err = _get_repo()
|
|
539
|
+
if err:
|
|
540
|
+
return f"Error: {err}"
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
from memvcs.core.collaboration import ContributionTracker
|
|
544
|
+
|
|
545
|
+
tracker = ContributionTracker(repo.mem_dir)
|
|
546
|
+
leaderboard = tracker.get_leaderboard(limit=limit)
|
|
547
|
+
|
|
548
|
+
if not leaderboard:
|
|
549
|
+
return "No contributions recorded yet."
|
|
550
|
+
|
|
551
|
+
lines = ["Contribution Leaderboard:"]
|
|
552
|
+
for entry in leaderboard:
|
|
553
|
+
lines.append(
|
|
554
|
+
f"#{entry['rank']} {entry['agent_id'][:8]}: {entry['commits']} commits"
|
|
555
|
+
)
|
|
556
|
+
return "\n".join(lines)
|
|
557
|
+
except Exception as e:
|
|
558
|
+
return f"Error: {e}"
|
|
559
|
+
|
|
560
|
+
# --- Compliance Tools ---
|
|
561
|
+
|
|
562
|
+
@mcp.tool()
|
|
563
|
+
def privacy_status(budget_name: Optional[str] = None) -> str:
|
|
564
|
+
"""Get privacy budget status.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
budget_name: Specific budget to check, or None for all
|
|
568
|
+
"""
|
|
569
|
+
repo, err = _get_repo()
|
|
570
|
+
if err:
|
|
571
|
+
return f"Error: {err}"
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
from memvcs.core.compliance import PrivacyManager
|
|
575
|
+
|
|
576
|
+
mgr = PrivacyManager(repo.mem_dir)
|
|
577
|
+
|
|
578
|
+
if budget_name:
|
|
579
|
+
budget = mgr.get_budget(budget_name)
|
|
580
|
+
if not budget:
|
|
581
|
+
return f"Budget not found: {budget_name}"
|
|
582
|
+
return f"Budget '{budget_name}': {budget.remaining():.2%} remaining ({budget.queries_made} queries)"
|
|
583
|
+
else:
|
|
584
|
+
data = mgr.get_dashboard_data()
|
|
585
|
+
if not data["budgets"]:
|
|
586
|
+
return "No privacy budgets configured."
|
|
587
|
+
lines = ["Privacy Budgets:"]
|
|
588
|
+
for b in data["budgets"]:
|
|
589
|
+
lines.append(f"- {b['name']}: {b['remaining']:.2%} remaining")
|
|
590
|
+
return "\n".join(lines)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
return f"Error: {e}"
|
|
593
|
+
|
|
594
|
+
@mcp.tool()
|
|
595
|
+
def integrity_verify() -> str:
|
|
596
|
+
"""Verify memory integrity using Merkle tree."""
|
|
597
|
+
repo, err = _get_repo()
|
|
598
|
+
if err:
|
|
599
|
+
return f"Error: {err}"
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
from memvcs.core.compliance import TamperDetector
|
|
603
|
+
|
|
604
|
+
detector = TamperDetector(repo.mem_dir)
|
|
605
|
+
result = detector.verify_integrity(repo.current_dir)
|
|
606
|
+
|
|
607
|
+
if result.get("error"):
|
|
608
|
+
# First time - store baseline
|
|
609
|
+
detector.store_merkle_state(repo.current_dir)
|
|
610
|
+
return "No baseline found. Created new integrity baseline."
|
|
611
|
+
|
|
612
|
+
if result["verified"]:
|
|
613
|
+
return "✓ Integrity verified. No tampering detected."
|
|
614
|
+
else:
|
|
615
|
+
lines = ["⚠ Integrity check failed:"]
|
|
616
|
+
if result["modified_files"]:
|
|
617
|
+
lines.append(f" Modified: {', '.join(result['modified_files'][:5])}")
|
|
618
|
+
if result["added_files"]:
|
|
619
|
+
lines.append(f" Added: {', '.join(result['added_files'][:5])}")
|
|
620
|
+
if result["deleted_files"]:
|
|
621
|
+
lines.append(f" Deleted: {', '.join(result['deleted_files'][:5])}")
|
|
622
|
+
return "\n".join(lines)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
return f"Error: {e}"
|
|
625
|
+
|
|
626
|
+
# --- Archaeology Tools ---
|
|
627
|
+
|
|
628
|
+
@mcp.tool()
|
|
629
|
+
def forgotten_memories(days: int = 30, limit: int = 10) -> str:
|
|
630
|
+
"""Find memories that haven't been accessed recently.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
days: Threshold for "forgotten" (default 30 days)
|
|
634
|
+
limit: Maximum results
|
|
635
|
+
"""
|
|
636
|
+
repo, err = _get_repo()
|
|
637
|
+
if err:
|
|
638
|
+
return f"Error: {err}"
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
from memvcs.core.archaeology import ForgottenKnowledgeFinder
|
|
642
|
+
|
|
643
|
+
finder = ForgottenKnowledgeFinder(repo.root)
|
|
644
|
+
forgotten = finder.find_forgotten(days_threshold=days, limit=limit)
|
|
645
|
+
|
|
646
|
+
if not forgotten:
|
|
647
|
+
return f"No memories older than {days} days found."
|
|
648
|
+
|
|
649
|
+
lines = [f"Forgotten memories (>{days} days):"]
|
|
650
|
+
for m in forgotten:
|
|
651
|
+
lines.append(f"- {m.path} ({m.days_since_access}d ago)")
|
|
652
|
+
lines.append(f" Preview: {m.content_preview[:60]}...")
|
|
653
|
+
return "\n".join(lines)
|
|
654
|
+
except Exception as e:
|
|
655
|
+
return f"Error: {e}"
|
|
656
|
+
|
|
657
|
+
@mcp.tool()
|
|
658
|
+
def find_context(path: str, date: str, window_days: int = 7) -> str:
|
|
659
|
+
"""Find what was happening around a memory at a point in time.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
path: Memory file path
|
|
663
|
+
date: Target date (YYYY-MM-DD format)
|
|
664
|
+
window_days: Days before/after to search
|
|
665
|
+
"""
|
|
666
|
+
repo, err = _get_repo()
|
|
667
|
+
if err:
|
|
668
|
+
return f"Error: {err}"
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
from memvcs.core.archaeology import ContextReconstructor
|
|
672
|
+
|
|
673
|
+
reconstructor = ContextReconstructor(repo.root)
|
|
674
|
+
context = reconstructor.reconstruct_context(path, date, window_days)
|
|
675
|
+
|
|
676
|
+
if context.get("error"):
|
|
677
|
+
return f"Error: {context['error']}"
|
|
678
|
+
|
|
679
|
+
return f"Context for {path} around {date}:\n{context['summary']}"
|
|
680
|
+
except Exception as e:
|
|
681
|
+
return f"Error: {e}"
|
|
682
|
+
|
|
683
|
+
# --- Confidence Tools ---
|
|
684
|
+
|
|
685
|
+
@mcp.tool()
|
|
686
|
+
def confidence_score(path: str, source_id: Optional[str] = None) -> str:
|
|
687
|
+
"""Get or calculate confidence score for a memory.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
path: Memory file path
|
|
691
|
+
source_id: Optional source agent ID
|
|
692
|
+
"""
|
|
693
|
+
repo, err = _get_repo()
|
|
694
|
+
if err:
|
|
695
|
+
return f"Error: {err}"
|
|
696
|
+
|
|
697
|
+
try:
|
|
698
|
+
from memvcs.core.confidence import ConfidenceCalculator
|
|
699
|
+
|
|
700
|
+
calculator = ConfidenceCalculator(repo.mem_dir)
|
|
701
|
+
|
|
702
|
+
# Check file exists
|
|
703
|
+
full_path = repo.current_dir / path
|
|
704
|
+
created_at = None
|
|
705
|
+
if full_path.exists():
|
|
706
|
+
from datetime import datetime, timezone
|
|
707
|
+
|
|
708
|
+
mtime = full_path.stat().st_mtime
|
|
709
|
+
created_at = datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat()
|
|
710
|
+
|
|
711
|
+
score = calculator.calculate_score(path, source_id=source_id, created_at=created_at)
|
|
712
|
+
|
|
713
|
+
lines = [
|
|
714
|
+
f"Confidence for {path}: {score.score:.1%}",
|
|
715
|
+
f" Source reliability: {score.factors.source_reliability:.1%}",
|
|
716
|
+
f" Age: {score.factors.age_days:.1f} days",
|
|
717
|
+
f" Corroborations: {score.factors.corroboration_count}",
|
|
718
|
+
f" Contradictions: {score.factors.contradiction_count}",
|
|
719
|
+
]
|
|
720
|
+
return "\n".join(lines)
|
|
721
|
+
except Exception as e:
|
|
722
|
+
return f"Error: {e}"
|
|
723
|
+
|
|
724
|
+
@mcp.tool()
|
|
725
|
+
def low_confidence(threshold: float = 0.5, limit: int = 10) -> str:
|
|
726
|
+
"""Find memories with low confidence scores.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
threshold: Confidence threshold (0.0-1.0)
|
|
730
|
+
limit: Maximum results
|
|
731
|
+
"""
|
|
732
|
+
repo, err = _get_repo()
|
|
733
|
+
if err:
|
|
734
|
+
return f"Error: {err}"
|
|
735
|
+
|
|
736
|
+
try:
|
|
737
|
+
from memvcs.core.confidence import ConfidenceCalculator
|
|
738
|
+
|
|
739
|
+
calculator = ConfidenceCalculator(repo.mem_dir)
|
|
740
|
+
low = calculator.get_low_confidence_memories(threshold=threshold)
|
|
741
|
+
|
|
742
|
+
if not low:
|
|
743
|
+
return f"No memories below {threshold:.0%} confidence."
|
|
744
|
+
|
|
745
|
+
lines = [f"Low confidence memories (<{threshold:.0%}):"]
|
|
746
|
+
for m in low[:limit]:
|
|
747
|
+
lines.append(f"- {m['path']}: {m['score']:.1%}")
|
|
748
|
+
return "\n".join(lines)
|
|
749
|
+
except Exception as e:
|
|
750
|
+
return f"Error: {e}"
|
|
751
|
+
|
|
752
|
+
@mcp.tool()
|
|
753
|
+
def expiring_soon(days: int = 7, threshold: float = 0.5) -> str:
|
|
754
|
+
"""Find memories that will drop below confidence threshold soon.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
days: Days to look ahead
|
|
758
|
+
threshold: Confidence threshold
|
|
759
|
+
"""
|
|
760
|
+
repo, err = _get_repo()
|
|
761
|
+
if err:
|
|
762
|
+
return f"Error: {err}"
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
from memvcs.core.confidence import ConfidenceCalculator
|
|
766
|
+
|
|
767
|
+
calculator = ConfidenceCalculator(repo.mem_dir)
|
|
768
|
+
expiring = calculator.get_expiring_soon(days=days, threshold=threshold)
|
|
769
|
+
|
|
770
|
+
if not expiring:
|
|
771
|
+
return f"No memories expiring in the next {days} days."
|
|
772
|
+
|
|
773
|
+
lines = [f"Expiring within {days} days:"]
|
|
774
|
+
for m in expiring:
|
|
775
|
+
lines.append(
|
|
776
|
+
f"- {m['path']}: {m['current_score']:.1%} -> <{threshold:.0%} in {m['days_until_threshold']:.1f}d"
|
|
777
|
+
)
|
|
778
|
+
return "\n".join(lines)
|
|
779
|
+
except Exception as e:
|
|
780
|
+
return f"Error: {e}"
|
|
781
|
+
|
|
782
|
+
# --- Time Travel Tools ---
|
|
783
|
+
|
|
784
|
+
@mcp.tool()
|
|
785
|
+
def time_travel(time_expr: str) -> str:
|
|
786
|
+
"""Find memory state at a specific time.
|
|
787
|
+
|
|
788
|
+
Args:
|
|
789
|
+
time_expr: Time expression like "2 days ago", "yesterday", "2024-01-15"
|
|
790
|
+
"""
|
|
791
|
+
repo, err = _get_repo()
|
|
792
|
+
if err:
|
|
793
|
+
return f"Error: {err}"
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
from memvcs.core.timetravel import TemporalNavigator
|
|
797
|
+
|
|
798
|
+
navigator = TemporalNavigator(repo.root)
|
|
799
|
+
commit = navigator.find_commit_at(time_expr)
|
|
800
|
+
|
|
801
|
+
if not commit:
|
|
802
|
+
return f"No commits found at or before: {time_expr}"
|
|
803
|
+
|
|
804
|
+
return f"At {time_expr}:\nCommit: {commit['short_hash']}\nMessage: {commit['message']}\nTime: {commit.get('timestamp', 'unknown')}"
|
|
805
|
+
except Exception as e:
|
|
806
|
+
return f"Error: {e}"
|
|
807
|
+
|
|
808
|
+
@mcp.tool()
|
|
809
|
+
def timeline(days: int = 30) -> str:
|
|
810
|
+
"""Get activity timeline.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
days: Number of days to include
|
|
814
|
+
"""
|
|
815
|
+
repo, err = _get_repo()
|
|
816
|
+
if err:
|
|
817
|
+
return f"Error: {err}"
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
from memvcs.core.timetravel import TimelineVisualizer
|
|
821
|
+
|
|
822
|
+
visualizer = TimelineVisualizer(repo.root)
|
|
823
|
+
timeline_data = visualizer.get_activity_timeline(days=days)
|
|
824
|
+
|
|
825
|
+
if not timeline_data:
|
|
826
|
+
return f"No activity in the last {days} days."
|
|
827
|
+
|
|
828
|
+
lines = [f"Activity (last {days} days):"]
|
|
829
|
+
for day in timeline_data[-10:]:
|
|
830
|
+
lines.append(f" {day['period']}: {day['count']} commits")
|
|
831
|
+
|
|
832
|
+
total = sum(d["count"] for d in timeline_data)
|
|
833
|
+
lines.append(f"\nTotal: {total} commits")
|
|
834
|
+
return "\n".join(lines)
|
|
835
|
+
except Exception as e:
|
|
836
|
+
return f"Error: {e}"
|
|
837
|
+
|
|
838
|
+
# --- Semantic Graph Tools ---
|
|
839
|
+
|
|
840
|
+
@mcp.tool()
|
|
841
|
+
def memory_graph(limit: int = 20) -> str:
|
|
842
|
+
"""Get semantic memory graph summary.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
limit: Maximum nodes to include
|
|
846
|
+
"""
|
|
847
|
+
repo, err = _get_repo()
|
|
848
|
+
if err:
|
|
849
|
+
return f"Error: {err}"
|
|
850
|
+
|
|
851
|
+
try:
|
|
852
|
+
from memvcs.core.semantic_graph import get_semantic_graph_dashboard
|
|
853
|
+
|
|
854
|
+
dashboard = get_semantic_graph_dashboard(repo.root)
|
|
855
|
+
|
|
856
|
+
lines = [
|
|
857
|
+
f"Memory Graph: {dashboard['node_count']} nodes, {dashboard['edge_count']} edges",
|
|
858
|
+
"",
|
|
859
|
+
"By Type:",
|
|
860
|
+
]
|
|
861
|
+
for t, count in dashboard.get("clusters_by_type", {}).items():
|
|
862
|
+
lines.append(f" {t}: {count}")
|
|
863
|
+
|
|
864
|
+
top_tags = list(dashboard.get("clusters_by_tag", {}).items())[:5]
|
|
865
|
+
if top_tags:
|
|
866
|
+
lines.append("\nTop Tags:")
|
|
867
|
+
for tag, count in top_tags:
|
|
868
|
+
lines.append(f" #{tag}: {count} memories")
|
|
869
|
+
|
|
870
|
+
return "\n".join(lines)
|
|
871
|
+
except Exception as e:
|
|
872
|
+
return f"Error: {e}"
|
|
873
|
+
|
|
874
|
+
@mcp.tool()
|
|
875
|
+
def graph_related(path: str, depth: int = 2) -> str:
|
|
876
|
+
"""Find related memories using graph traversal.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
path: Memory file path
|
|
880
|
+
depth: Maximum traversal depth
|
|
881
|
+
"""
|
|
882
|
+
repo, err = _get_repo()
|
|
883
|
+
if err:
|
|
884
|
+
return f"Error: {err}"
|
|
885
|
+
|
|
886
|
+
try:
|
|
887
|
+
from memvcs.core.semantic_graph import SemanticGraphBuilder, GraphSearchEngine
|
|
888
|
+
|
|
889
|
+
builder = SemanticGraphBuilder(repo.root)
|
|
890
|
+
nodes, edges = builder.build_graph()
|
|
891
|
+
|
|
892
|
+
# Find node by path
|
|
893
|
+
node_id = None
|
|
894
|
+
for n in nodes:
|
|
895
|
+
if n.path == path or path in n.path:
|
|
896
|
+
node_id = n.node_id
|
|
897
|
+
break
|
|
898
|
+
|
|
899
|
+
if not node_id:
|
|
900
|
+
return f"Memory not found: {path}"
|
|
901
|
+
|
|
902
|
+
nodes_dict = {n.node_id: n for n in nodes}
|
|
903
|
+
engine = GraphSearchEngine(nodes_dict, edges)
|
|
904
|
+
related = engine.find_related(node_id, max_depth=depth, limit=10)
|
|
905
|
+
|
|
906
|
+
if not related:
|
|
907
|
+
return "No related memories found."
|
|
908
|
+
|
|
909
|
+
lines = [f"Related to {path}:"]
|
|
910
|
+
for node, score, dist in related:
|
|
911
|
+
lines.append(f" {node.path} (score: {score:.2f}, depth: {dist})")
|
|
912
|
+
return "\n".join(lines)
|
|
913
|
+
except Exception as e:
|
|
914
|
+
return f"Error: {e}"
|
|
915
|
+
|
|
916
|
+
# --- Agent Tools ---
|
|
917
|
+
|
|
918
|
+
@mcp.tool()
|
|
919
|
+
def agent_health() -> str:
|
|
920
|
+
"""Run memory health check."""
|
|
921
|
+
repo, err = _get_repo()
|
|
922
|
+
if err:
|
|
923
|
+
return f"Error: {err}"
|
|
924
|
+
|
|
925
|
+
try:
|
|
926
|
+
from memvcs.core.agents import MemoryAgentManager
|
|
927
|
+
|
|
928
|
+
manager = MemoryAgentManager(repo.root)
|
|
929
|
+
health = manager.run_health_check()
|
|
930
|
+
|
|
931
|
+
lines = ["Memory Health Check:"]
|
|
932
|
+
|
|
933
|
+
cons = health["checks"]["consolidation"]
|
|
934
|
+
lines.append(f" Consolidation candidates: {cons['candidate_count']}")
|
|
935
|
+
|
|
936
|
+
clean = health["checks"]["cleanup"]
|
|
937
|
+
lines.append(f" Cleanup candidates: {clean['candidate_count']}")
|
|
938
|
+
|
|
939
|
+
dups = health["checks"]["duplicates"]
|
|
940
|
+
lines.append(f" Duplicate groups: {dups['duplicate_groups']}")
|
|
941
|
+
|
|
942
|
+
alerts = health.get("alerts", [])
|
|
943
|
+
lines.append(f" Active alerts: {len(alerts)}")
|
|
944
|
+
|
|
945
|
+
return "\n".join(lines)
|
|
946
|
+
except Exception as e:
|
|
947
|
+
return f"Error: {e}"
|
|
948
|
+
|
|
949
|
+
@mcp.tool()
|
|
950
|
+
def find_duplicates() -> str:
|
|
951
|
+
"""Find duplicate memory files."""
|
|
952
|
+
repo, err = _get_repo()
|
|
953
|
+
if err:
|
|
954
|
+
return f"Error: {err}"
|
|
955
|
+
|
|
956
|
+
try:
|
|
957
|
+
from memvcs.core.agents import CleanupAgent
|
|
958
|
+
|
|
959
|
+
agent = CleanupAgent(repo.root)
|
|
960
|
+
duplicates = agent.find_duplicates()
|
|
961
|
+
|
|
962
|
+
if not duplicates:
|
|
963
|
+
return "No duplicate memories found."
|
|
964
|
+
|
|
965
|
+
lines = [f"Found {len(duplicates)} duplicate groups:"]
|
|
966
|
+
for dup in duplicates[:10]:
|
|
967
|
+
lines.append(f"\nGroup ({dup['count']} files):")
|
|
968
|
+
for f in dup["files"][:3]:
|
|
969
|
+
lines.append(f" - {f}")
|
|
970
|
+
return "\n".join(lines)
|
|
971
|
+
except Exception as e:
|
|
972
|
+
return f"Error: {e}"
|
|
973
|
+
|
|
974
|
+
@mcp.tool()
|
|
975
|
+
def consolidation_candidates() -> str:
|
|
976
|
+
"""Find memories that could be consolidated."""
|
|
977
|
+
repo, err = _get_repo()
|
|
978
|
+
if err:
|
|
979
|
+
return f"Error: {err}"
|
|
980
|
+
|
|
981
|
+
try:
|
|
982
|
+
from memvcs.core.agents import ConsolidationAgent
|
|
983
|
+
|
|
984
|
+
agent = ConsolidationAgent(repo.root)
|
|
985
|
+
candidates = agent.find_consolidation_candidates()
|
|
986
|
+
|
|
987
|
+
if not candidates:
|
|
988
|
+
return "No consolidation candidates found."
|
|
989
|
+
|
|
990
|
+
lines = ["Consolidation candidates:"]
|
|
991
|
+
for c in candidates[:10]:
|
|
992
|
+
lines.append(f"\n{c['prefix']}: {c['file_count']} files")
|
|
993
|
+
lines.append(f" {c['suggestion']}")
|
|
994
|
+
return "\n".join(lines)
|
|
995
|
+
except Exception as e:
|
|
996
|
+
return f"Error: {e}"
|
|
997
|
+
|
|
998
|
+
@mcp.tool()
|
|
999
|
+
def cleanup_candidates(max_age_days: int = 90) -> str:
|
|
1000
|
+
"""Find old memories that could be cleaned up.
|
|
1001
|
+
|
|
1002
|
+
Args:
|
|
1003
|
+
max_age_days: Minimum age in days
|
|
1004
|
+
"""
|
|
1005
|
+
repo, err = _get_repo()
|
|
1006
|
+
if err:
|
|
1007
|
+
return f"Error: {err}"
|
|
1008
|
+
|
|
1009
|
+
try:
|
|
1010
|
+
from memvcs.core.agents import CleanupAgent
|
|
1011
|
+
|
|
1012
|
+
agent = CleanupAgent(repo.root)
|
|
1013
|
+
candidates = agent.find_cleanup_candidates(max_age_days=max_age_days)
|
|
1014
|
+
|
|
1015
|
+
if not candidates:
|
|
1016
|
+
return f"No memories older than {max_age_days} days."
|
|
1017
|
+
|
|
1018
|
+
lines = [f"Old memories (>{max_age_days} days):"]
|
|
1019
|
+
for c in candidates[:10]:
|
|
1020
|
+
lines.append(f" {c['path']} ({c['age_days']}d old)")
|
|
1021
|
+
return "\n".join(lines)
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
return f"Error: {e}"
|
|
1024
|
+
|
|
254
1025
|
return mcp
|
|
255
1026
|
|
|
256
1027
|
|