agentcache-core 0.9.9__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 (52) hide show
  1. agentcache/__init__.py +29 -0
  2. agentcache/app.py +312 -0
  3. agentcache/cli.py +346 -0
  4. agentcache/connect.py +724 -0
  5. agentcache/core/__init__.py +19 -0
  6. agentcache/core/audit_log.py +104 -0
  7. agentcache/core/config.py +51 -0
  8. agentcache/core/context_builder.py +209 -0
  9. agentcache/core/graph.py +120 -0
  10. agentcache/core/image_store.py +111 -0
  11. agentcache/core/infer.py +142 -0
  12. agentcache/core/kv_scopes.py +86 -0
  13. agentcache/core/lessons.py +242 -0
  14. agentcache/core/llm.py +596 -0
  15. agentcache/core/memory_store.py +207 -0
  16. agentcache/core/observation_store.py +576 -0
  17. agentcache/core/privacy.py +34 -0
  18. agentcache/core/project_profile.py +625 -0
  19. agentcache/core/search_service.py +444 -0
  20. agentcache/core/session_store.py +382 -0
  21. agentcache/core/slots.py +504 -0
  22. agentcache/db.py +359 -0
  23. agentcache/import_data.py +86 -0
  24. agentcache/legacy.py +94 -0
  25. agentcache/mcp_stdio.py +141 -0
  26. agentcache/py.typed +0 -0
  27. agentcache/replay_import.py +680 -0
  28. agentcache/routes/__init__.py +29 -0
  29. agentcache/routes/_deps.py +46 -0
  30. agentcache/routes/auth.py +68 -0
  31. agentcache/routes/graph.py +81 -0
  32. agentcache/routes/health.py +149 -0
  33. agentcache/routes/mcp.py +614 -0
  34. agentcache/routes/memories.py +116 -0
  35. agentcache/routes/migration.py +32 -0
  36. agentcache/routes/observations.py +253 -0
  37. agentcache/routes/search.py +80 -0
  38. agentcache/search.py +935 -0
  39. agentcache/storage/__init__.py +29 -0
  40. agentcache/storage/images.py +102 -0
  41. agentcache/storage/paths.py +116 -0
  42. agentcache/storage/scopes.py +9 -0
  43. agentcache/viewer/favicon.svg +1 -0
  44. agentcache/viewer/index.html +4235 -0
  45. agentcache/viewer_helpers.py +61 -0
  46. agentcache/workers.py +192 -0
  47. agentcache_core-0.9.9.dist-info/METADATA +194 -0
  48. agentcache_core-0.9.9.dist-info/RECORD +52 -0
  49. agentcache_core-0.9.9.dist-info/WHEEL +5 -0
  50. agentcache_core-0.9.9.dist-info/entry_points.txt +2 -0
  51. agentcache_core-0.9.9.dist-info/licenses/LICENSE +190 -0
  52. agentcache_core-0.9.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,614 @@
1
+ """
2
+ MCP (Model Context Protocol) routes blueprint.
3
+
4
+ Handles:
5
+ GET /agentmemory/mcp/tools — list all MCP tool schemas
6
+ POST /agentmemory/mcp/tools — dispatch a tool call
7
+ """
8
+
9
+ import datetime
10
+ import json
11
+ import os
12
+
13
+ from flask import Blueprint, jsonify, request
14
+
15
+ from .. import legacy
16
+ from ..core import KV
17
+ from ._deps import get_kv, get_observation_store, get_search_service
18
+ from .auth import require_auth
19
+
20
+ mcp_bp = Blueprint("mcp", __name__)
21
+
22
+
23
+ def _datetime_now_iso() -> str:
24
+ return (
25
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
26
+ )
27
+
28
+
29
+ def _parse_mcp_list_arg(arg_val):
30
+ """Accept a list or comma-separated string and return a list of strings."""
31
+ if isinstance(arg_val, list):
32
+ return [str(item).strip() for item in arg_val if item]
33
+ if isinstance(arg_val, str) and arg_val:
34
+ return [item.strip() for item in arg_val.split(",") if item.strip()]
35
+ return []
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # GET /agentcache/mcp/tools
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ def get_mcp_tools_schemas():
44
+ return [
45
+ {
46
+ "name": "cache_recall",
47
+ "description": "Search past folder observations and global memories. Use when you need to recall what happened in a folder.",
48
+ "inputSchema": {
49
+ "type": "object",
50
+ "properties": {
51
+ "query": {"type": "string", "description": "Search query keywords"},
52
+ "limit": {
53
+ "type": "number",
54
+ "description": "Max results to return (default 10)",
55
+ },
56
+ "folderPath": {
57
+ "type": "string",
58
+ "description": "Filter to a specific folder path (optional)",
59
+ },
60
+ "agentId": {
61
+ "type": "string",
62
+ "description": "Filter to a specific agent ID (optional)",
63
+ },
64
+ },
65
+ "required": ["query"],
66
+ },
67
+ },
68
+ {
69
+ "name": "cache_smart_search",
70
+ "description": "Hybrid semantic+keyword search across folder observations and global memories.",
71
+ "inputSchema": {
72
+ "type": "object",
73
+ "properties": {
74
+ "query": {"type": "string", "description": "Search query"},
75
+ "limit": {
76
+ "type": "number",
77
+ "description": "Max results (default 10)",
78
+ },
79
+ "folderPath": {
80
+ "type": "string",
81
+ "description": "Filter to a specific folder path (optional)",
82
+ },
83
+ "agentId": {
84
+ "type": "string",
85
+ "description": "Filter to a specific agent ID (optional)",
86
+ },
87
+ },
88
+ "required": ["query"],
89
+ },
90
+ },
91
+ {
92
+ "name": "cache_save",
93
+ "description": "Explicitly save an important insight, decision, or pattern to long-term memory.",
94
+ "inputSchema": {
95
+ "type": "object",
96
+ "properties": {
97
+ "content": {
98
+ "type": "string",
99
+ "description": "The insight or decision to remember",
100
+ },
101
+ "type": {
102
+ "type": "string",
103
+ "description": "Memory type: pattern, preference, architecture, bug, workflow, or fact",
104
+ },
105
+ "concepts": {
106
+ "oneOf": [
107
+ {
108
+ "type": "string",
109
+ "description": "Comma-separated key concepts",
110
+ },
111
+ {
112
+ "type": "array",
113
+ "items": {"type": "string"},
114
+ "description": "List of key concepts",
115
+ },
116
+ ]
117
+ },
118
+ "files": {
119
+ "oneOf": [
120
+ {
121
+ "type": "string",
122
+ "description": "Comma-separated relevant file paths",
123
+ },
124
+ {
125
+ "type": "array",
126
+ "items": {"type": "string"},
127
+ "description": "List of relevant file paths",
128
+ },
129
+ ]
130
+ },
131
+ "project": {
132
+ "type": "string",
133
+ "description": "Canonical project identifier",
134
+ },
135
+ },
136
+ "required": ["content"],
137
+ },
138
+ },
139
+ {
140
+ "name": "cache_diagnose",
141
+ "description": "Health check — returns folder, agent, observation and memory counts.",
142
+ "inputSchema": {"type": "object", "properties": {}},
143
+ },
144
+ {
145
+ "name": "cache_forget",
146
+ "description": "Delete a global memory or all observations for a (folderPath, agentId) pair.",
147
+ "inputSchema": {
148
+ "type": "object",
149
+ "properties": {
150
+ "memoryId": {
151
+ "type": "string",
152
+ "description": "Memory ID to delete",
153
+ },
154
+ "folderPath": {
155
+ "type": "string",
156
+ "description": "Folder path to delete observations from",
157
+ },
158
+ "agentId": {
159
+ "type": "string",
160
+ "description": "Agent ID to delete observations for",
161
+ },
162
+ "observationIds": {
163
+ "oneOf": [
164
+ {
165
+ "type": "string",
166
+ "description": "Comma-separated observation IDs to delete",
167
+ },
168
+ {"type": "array", "items": {"type": "string"}},
169
+ ]
170
+ },
171
+ },
172
+ },
173
+ },
174
+ {
175
+ "name": "cache_export",
176
+ "description": "Export all folder observations and global memories as JSON (v2 format).",
177
+ "inputSchema": {"type": "object", "properties": {}},
178
+ },
179
+ {
180
+ "name": "agent_observe",
181
+ "description": "Log an observation scoped to a folder path and agent ID.",
182
+ "inputSchema": {
183
+ "type": "object",
184
+ "properties": {
185
+ "folderPath": {
186
+ "type": "string",
187
+ "description": "Absolute path of the working directory",
188
+ },
189
+ "agentId": {
190
+ "type": "string",
191
+ "description": "Identity of the agent (e.g. 'kiro', 'claude')",
192
+ },
193
+ "text": {"type": "string", "description": "Observation content"},
194
+ "timestamp": {
195
+ "type": "string",
196
+ "description": "ISO 8601 UTC timestamp (defaults to now)",
197
+ },
198
+ "type": {"type": "string", "description": "Observation type"},
199
+ "title": {"type": "string", "description": "Short title"},
200
+ "concepts": {"type": "array", "items": {"type": "string"}},
201
+ "files": {"type": "array", "items": {"type": "string"}},
202
+ "importance": {"type": "integer", "description": "1-10"},
203
+ "sessionId": {
204
+ "type": "string",
205
+ "description": "Deprecated — ignored",
206
+ },
207
+ },
208
+ "required": ["folderPath", "agentId", "text"],
209
+ },
210
+ },
211
+ {
212
+ "name": "agent_cache",
213
+ "description": "Explicitly save a key insight, fact, or architecture decision to long-term memory.",
214
+ "inputSchema": {
215
+ "type": "object",
216
+ "properties": {
217
+ "agentId": {
218
+ "type": "string",
219
+ "description": "ID/Name of the agent (optional)",
220
+ },
221
+ "content": {
222
+ "type": "string",
223
+ "description": "The memory content/insight",
224
+ },
225
+ "project": {
226
+ "type": "string",
227
+ "description": "Canonical project path/identifier",
228
+ },
229
+ "type": {
230
+ "type": "string",
231
+ "description": "Memory type: fact, preference, bug, workflow, architecture",
232
+ },
233
+ "concepts": {
234
+ "oneOf": [
235
+ {
236
+ "type": "string",
237
+ "description": "Comma-separated key concepts",
238
+ },
239
+ {"type": "array", "items": {"type": "string"}},
240
+ ]
241
+ },
242
+ "files": {
243
+ "oneOf": [
244
+ {
245
+ "type": "string",
246
+ "description": "Comma-separated relevant file paths",
247
+ },
248
+ {"type": "array", "items": {"type": "string"}},
249
+ ]
250
+ },
251
+ },
252
+ "required": ["content"],
253
+ },
254
+ },
255
+ {
256
+ "name": "cache_folders",
257
+ "description": "List all (folder, agent) pairs that have memory observations.",
258
+ "inputSchema": {"type": "object", "properties": {}},
259
+ },
260
+ {
261
+ "name": "cache_folder_observations",
262
+ "description": "Get all observations for a specific (folderPath, agentId) pair.",
263
+ "inputSchema": {
264
+ "type": "object",
265
+ "properties": {
266
+ "folderPath": {"type": "string", "description": "Folder path"},
267
+ "agentId": {"type": "string", "description": "Agent ID"},
268
+ },
269
+ "required": ["folderPath", "agentId"],
270
+ },
271
+ },
272
+ {
273
+ "name": "cache_timeline",
274
+ "description": "Get folder activity feed — observations sorted by time, filterable by folder/agent.",
275
+ "inputSchema": {
276
+ "type": "object",
277
+ "properties": {
278
+ "folderPath": {
279
+ "type": "string",
280
+ "description": "Filter by folder path (optional)",
281
+ },
282
+ "agentId": {
283
+ "type": "string",
284
+ "description": "Filter by agent ID (optional)",
285
+ },
286
+ "limit": {
287
+ "type": "integer",
288
+ "description": "Max results (default 100)",
289
+ },
290
+ "before": {
291
+ "type": "string",
292
+ "description": "ISO timestamp upper bound (optional)",
293
+ },
294
+ "after": {
295
+ "type": "string",
296
+ "description": "ISO timestamp lower bound (optional)",
297
+ },
298
+ },
299
+ },
300
+ },
301
+ {
302
+ "name": "cache_dedup",
303
+ "description": "Remove duplicate observations from a (folderPath, agentId) pair or all pairs. Keeps the earliest observation per unique text fingerprint.",
304
+ "inputSchema": {
305
+ "type": "object",
306
+ "properties": {
307
+ "folderPath": {
308
+ "type": "string",
309
+ "description": "Folder path to deduplicate (optional — omit for all pairs)",
310
+ },
311
+ "agentId": {
312
+ "type": "string",
313
+ "description": "Agent ID to deduplicate (optional — omit for all pairs)",
314
+ },
315
+ },
316
+ },
317
+ },
318
+ ]
319
+
320
+
321
+ @mcp_bp.route("/agentcache/mcp/tools", methods=["GET"])
322
+ @mcp_bp.route("/agentmemory/mcp/tools", methods=["GET"])
323
+ @require_auth
324
+ def mcp_tools_list():
325
+ tools = get_mcp_tools_schemas()
326
+ return jsonify({"tools": tools}), 200
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # POST /agentcache/mcp/tools
331
+ # ---------------------------------------------------------------------------
332
+
333
+
334
+ @mcp_bp.route("/agentcache/mcp/tools", methods=["POST"])
335
+ @mcp_bp.route("/agentmemory/mcp/tools", methods=["POST"])
336
+ @require_auth
337
+ def mcp_tools_call():
338
+ try:
339
+ kv = get_kv()
340
+ body = request.get_json(force=True) or {}
341
+ name = body.get("name")
342
+ args = body.get("arguments") or {}
343
+ if not name:
344
+ return jsonify({"error": "name is required"}), 400
345
+
346
+ print(f"[mcp] Calling tool {name} with args: {args}")
347
+ text_out = ""
348
+
349
+ if name in ("cache_recall", "memory_recall"):
350
+ q = args.get("query")
351
+ limit = int(args.get("limit") or 10)
352
+ folder_path = args.get("folderPath")
353
+ agent_id = args.get("agentId")
354
+ if legacy.is_agent_scope_isolated():
355
+ current_aid = legacy.get_agent_id()
356
+ if current_aid:
357
+ if agent_id and agent_id != current_aid:
358
+ return jsonify(
359
+ {"error": "Unauthorized: Agent scope is isolated"}
360
+ ), 403
361
+ agent_id = current_aid
362
+ search_svc = get_search_service()
363
+ if search_svc is not None:
364
+ res = search_svc.search(
365
+ query=q,
366
+ limit=limit,
367
+ folder_path=folder_path,
368
+ agent_id=agent_id,
369
+ kv=kv,
370
+ )
371
+ else:
372
+ res = []
373
+ text_out = json.dumps(res, indent=2)
374
+
375
+ elif name in ("cache_save", "memory_save"):
376
+ content = args.get("content")
377
+ concepts = _parse_mcp_list_arg(args.get("concepts"))
378
+ files = _parse_mcp_list_arg(args.get("files"))
379
+ project = args.get("project")
380
+ res = legacy.remember(
381
+ kv,
382
+ {
383
+ "content": content,
384
+ "type": args.get("type") or "fact",
385
+ "concepts": concepts,
386
+ "files": files,
387
+ "project": project,
388
+ },
389
+ )
390
+ text_out = json.dumps(res)
391
+
392
+ elif name in ("cache_smart_search", "memory_smart_search"):
393
+ q = args.get("query")
394
+ limit = int(args.get("limit") or 10)
395
+ folder_path = args.get("folderPath")
396
+ agent_id = args.get("agentId")
397
+ if legacy.is_agent_scope_isolated():
398
+ current_aid = legacy.get_agent_id()
399
+ if current_aid:
400
+ if agent_id and agent_id != current_aid:
401
+ return jsonify(
402
+ {"error": "Unauthorized: Agent scope is isolated"}
403
+ ), 403
404
+ agent_id = current_aid
405
+ search_svc = get_search_service()
406
+ if search_svc is not None:
407
+ res = search_svc.search(
408
+ query=q,
409
+ limit=limit,
410
+ folder_path=folder_path,
411
+ agent_id=agent_id,
412
+ kv=kv,
413
+ )
414
+ else:
415
+ res = []
416
+ text_out = json.dumps(res, indent=2)
417
+
418
+ elif name in ("cache_diagnose", "memory_diagnose"):
419
+ res = legacy.health_check(kv)
420
+ text_out = json.dumps(res, indent=2)
421
+
422
+ elif name in ("cache_forget", "memory_forget"):
423
+ obs_ids = _parse_mcp_list_arg(args.get("observationIds"))
424
+ request_aid = args.get("agentId")
425
+ if legacy.is_agent_scope_isolated():
426
+ current_aid = legacy.get_agent_id()
427
+ if current_aid:
428
+ if request_aid and request_aid != current_aid:
429
+ return jsonify(
430
+ {"error": "Unauthorized: Agent scope is isolated"}
431
+ ), 403
432
+ request_aid = current_aid
433
+ obs_store = get_observation_store()
434
+ if obs_store is not None:
435
+ forget_payload = {
436
+ "memoryId": args.get("memoryId"),
437
+ "folderPath": args.get("folderPath"),
438
+ "agentId": request_aid,
439
+ }
440
+ if obs_ids:
441
+ forget_payload["observationIds"] = obs_ids
442
+ res = obs_store.forget(forget_payload)
443
+ else:
444
+ res = {"success": False, "deleted": 0}
445
+ text_out = json.dumps(res, indent=2)
446
+
447
+ elif name in ("cache_export", "memory_export"):
448
+ res = legacy.export_data(kv, {})
449
+ text_out = json.dumps(res, indent=2)
450
+
451
+ elif name == "agent_observe":
452
+ folder_path = args.get("folderPath")
453
+ agent_id = args.get("agentId")
454
+ text = args.get("text") or args.get("content") or ""
455
+
456
+ if not folder_path:
457
+ folder_path = (
458
+ args.get("cwd")
459
+ or args.get("project")
460
+ or os.getenv("AGENTCACHE_CWD")
461
+ or os.getenv("AGENTMEMORY_CWD")
462
+ or "/unknown"
463
+ )
464
+ if not agent_id:
465
+ agent_id = (
466
+ args.get("sessionId")
467
+ or legacy.get_agent_id()
468
+ or os.getenv("AGENT_ID")
469
+ or "agent"
470
+ )
471
+
472
+ if legacy.is_agent_scope_isolated():
473
+ current_aid = legacy.get_agent_id()
474
+ if current_aid:
475
+ if agent_id and agent_id != current_aid:
476
+ return jsonify(
477
+ {"error": "Unauthorized: Agent scope is isolated"}
478
+ ), 403
479
+ agent_id = current_aid
480
+
481
+ if not text:
482
+ return jsonify({"error": "text (or content) is required"}), 400
483
+
484
+ timestamp = args.get("timestamp") or _datetime_now_iso()
485
+ payload = {
486
+ "folderPath": folder_path,
487
+ "agentId": agent_id,
488
+ "text": text,
489
+ "timestamp": timestamp,
490
+ "type": args.get("type"),
491
+ "title": args.get("title"),
492
+ "concepts": args.get("concepts"),
493
+ "files": args.get("files"),
494
+ "importance": args.get("importance"),
495
+ }
496
+ obs_store = get_observation_store()
497
+ if obs_store is not None:
498
+ res = obs_store.ingest(payload)
499
+ else:
500
+ res = {"error": "ObservationStore not initialized"}
501
+ text_out = json.dumps(res)
502
+
503
+ elif name in ("agent_cache", "agent_remember"):
504
+ agent_id = args.get("agentId") or legacy.get_agent_id() or "agent"
505
+ content = args.get("content")
506
+ project = args.get("project")
507
+ mem_type = args.get("type") or "fact"
508
+ concepts = _parse_mcp_list_arg(args.get("concepts"))
509
+ files = _parse_mcp_list_arg(args.get("files"))
510
+
511
+ if legacy.is_agent_scope_isolated():
512
+ current_aid = legacy.get_agent_id()
513
+ if current_aid:
514
+ if agent_id and agent_id != current_aid:
515
+ return jsonify(
516
+ {"error": "Unauthorized: Agent scope is isolated"}
517
+ ), 403
518
+ agent_id = current_aid
519
+
520
+ if not content:
521
+ return jsonify({"error": "content is required"}), 400
522
+ payload = {
523
+ "content": content,
524
+ "type": mem_type,
525
+ "concepts": concepts,
526
+ "files": files,
527
+ "project": project,
528
+ "agentId": agent_id,
529
+ }
530
+ res = legacy.remember(kv, payload)
531
+ text_out = json.dumps(res)
532
+
533
+ elif name in ("cache_folders", "memory_folders"):
534
+ pairs = sorted(
535
+ kv.list(KV.folders),
536
+ key=lambda x: x.get("lastUpdated", ""),
537
+ reverse=True,
538
+ )
539
+ if legacy.is_agent_scope_isolated():
540
+ aid = legacy.get_agent_id()
541
+ if aid:
542
+ pairs = [p for p in pairs if p.get("agentId") == aid]
543
+ text_out = json.dumps(pairs, indent=2)
544
+
545
+ elif name in ("cache_folder_observations", "memory_folder_observations"):
546
+ fp = args.get("folderPath", "")
547
+ aid = args.get("agentId", "")
548
+ if not fp or not aid:
549
+ return jsonify({"error": "folderPath and agentId are required"}), 400
550
+ if legacy.is_agent_scope_isolated():
551
+ current_aid = legacy.get_agent_id()
552
+ if current_aid and aid != current_aid:
553
+ return jsonify(
554
+ {"error": "Unauthorized: Agent scope is isolated"}
555
+ ), 403
556
+ obs = sorted(
557
+ kv.list(KV.folder_obs(fp, aid)),
558
+ key=lambda x: x.get("timestamp", ""),
559
+ reverse=True,
560
+ )
561
+ text_out = json.dumps(obs, indent=2)
562
+
563
+ elif name in ("cache_timeline", "memory_timeline"):
564
+ request_aid = args.get("agentId")
565
+ if legacy.is_agent_scope_isolated():
566
+ current_aid = legacy.get_agent_id()
567
+ if current_aid:
568
+ if request_aid and request_aid != current_aid:
569
+ return jsonify(
570
+ {"error": "Unauthorized: Agent scope is isolated"}
571
+ ), 403
572
+ request_aid = current_aid
573
+ obs_store = get_observation_store()
574
+ if obs_store is not None:
575
+ res = obs_store.timeline(
576
+ limit=int(args.get("limit", 100)),
577
+ folder_path=args.get("folderPath"),
578
+ agent_id=request_aid,
579
+ before=args.get("before"),
580
+ after=args.get("after"),
581
+ )
582
+ else:
583
+ res = []
584
+ text_out = json.dumps(res, indent=2)
585
+
586
+ elif name in ("cache_dedup", "memory_dedup"):
587
+ request_aid = args.get("agentId")
588
+ if legacy.is_agent_scope_isolated():
589
+ current_aid = legacy.get_agent_id()
590
+ if current_aid:
591
+ if request_aid and request_aid != current_aid:
592
+ return jsonify(
593
+ {"error": "Unauthorized: Agent scope is isolated"}
594
+ ), 403
595
+ request_aid = current_aid
596
+ obs_store = get_observation_store()
597
+ if obs_store is not None:
598
+ res = obs_store.dedup(
599
+ args.get("folderPath") or None,
600
+ request_aid or None,
601
+ )
602
+ else:
603
+ res = {"success": False}
604
+ text_out = json.dumps(res, indent=2)
605
+
606
+ text_out = json.dumps(res, indent=2)
607
+
608
+ else:
609
+ return jsonify({"error": f"unknown tool: {name}"}), 400
610
+
611
+ return jsonify({"content": [{"type": "text", "text": text_out}]}), 200
612
+
613
+ except Exception as e:
614
+ return jsonify({"error": str(e)}), 500