memctrl 1.0.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.
- memctrl/__init__.py +19 -0
- memctrl/cli.py +443 -0
- memctrl/extractor.py +261 -0
- memctrl/installer.py +122 -0
- memctrl/integrations/langgraph.py +269 -0
- memctrl/mcp_server.py +231 -0
- memctrl/retriever.py +267 -0
- memctrl/rules.py +330 -0
- memctrl/store.py +461 -0
- memctrl/templates/SKILL.md +63 -0
- memctrl/templates/__init__.py +0 -0
- memctrl/tree.py +257 -0
- memctrl-1.0.0.dist-info/METADATA +356 -0
- memctrl-1.0.0.dist-info/RECORD +17 -0
- memctrl-1.0.0.dist-info/WHEEL +4 -0
- memctrl-1.0.0.dist-info/entry_points.txt +2 -0
- memctrl-1.0.0.dist-info/licenses/LICENSE +21 -0
memctrl/tree.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""MemCtrl — PageIndex-style hierarchical tree builder.
|
|
2
|
+
|
|
3
|
+
Adapts PageIndex (VectifyAI) tree architecture for memory:
|
|
4
|
+
- Nodes: {node_id, title, layer, summary, memory_ids, children}
|
|
5
|
+
- LLM clusters flat memories into semantic groups per layer
|
|
6
|
+
- Produces explainable, inspectable hierarchy (no vectors)
|
|
7
|
+
|
|
8
|
+
Research: PageIndex uses {node_id, title, start_index, end_index, summary,
|
|
9
|
+
sub_nodes[]}. We replace page references with memory metadata.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import uuid
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Any, Callable, Coroutine, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from memctrl.store import TreeNode
|
|
21
|
+
|
|
22
|
+
# Type alias for async LLM callable
|
|
23
|
+
LLMCallable = Callable[[str, bool], Coroutine[Any, Any, str]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MemoryTreeBuilder:
|
|
27
|
+
"""PageIndex-inspired hierarchical tree builder for memories.
|
|
28
|
+
|
|
29
|
+
Algorithm:
|
|
30
|
+
1. Group memories by layer (project / session / user)
|
|
31
|
+
2. Within each layer, use LLM to cluster memories into semantic groups
|
|
32
|
+
3. Each cluster becomes a tree node with LLM-generated summary
|
|
33
|
+
4. Leaf nodes contain individual memory facts
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, llm_client: Optional[LLMCallable] = None):
|
|
37
|
+
self.llm_client = llm_client
|
|
38
|
+
|
|
39
|
+
# --- Public API ---
|
|
40
|
+
|
|
41
|
+
async def build_tree(self, memories: List[dict]) -> TreeNode:
|
|
42
|
+
"""Build tree from flat list of memory dicts.
|
|
43
|
+
|
|
44
|
+
memories: list of dicts with keys: id, layer, content, confidence
|
|
45
|
+
Returns root TreeNode with layer children.
|
|
46
|
+
"""
|
|
47
|
+
if not memories:
|
|
48
|
+
return TreeNode(
|
|
49
|
+
id="root", title="Memory Tree", layer="root",
|
|
50
|
+
summary="Empty memory store",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 1. Group by layer
|
|
54
|
+
by_layer = self._group_by_layer(memories)
|
|
55
|
+
|
|
56
|
+
# 2. Build layer nodes (with LLM clustering inside each)
|
|
57
|
+
layer_nodes: List[TreeNode] = []
|
|
58
|
+
for layer_name, mems in by_layer.items():
|
|
59
|
+
if self.llm_client:
|
|
60
|
+
node = await self._cluster_with_llm(layer_name, mems)
|
|
61
|
+
else:
|
|
62
|
+
node = self._cluster_fallback(layer_name, mems)
|
|
63
|
+
layer_nodes.append(node)
|
|
64
|
+
|
|
65
|
+
# 3. Build root
|
|
66
|
+
root = TreeNode(
|
|
67
|
+
id="root",
|
|
68
|
+
title="Memory Tree",
|
|
69
|
+
layer="root",
|
|
70
|
+
summary=f"Root node with {len(layer_nodes)} layers, "
|
|
71
|
+
f"{len(memories)} total memories",
|
|
72
|
+
children=layer_nodes,
|
|
73
|
+
)
|
|
74
|
+
return root
|
|
75
|
+
|
|
76
|
+
# --- Grouping ---
|
|
77
|
+
|
|
78
|
+
def _group_by_layer(self, memories: List[dict]) -> Dict[str, List[dict]]:
|
|
79
|
+
result: Dict[str, List[dict]] = {}
|
|
80
|
+
for mem in memories:
|
|
81
|
+
layer = mem.get("layer", "session")
|
|
82
|
+
result.setdefault(layer, []).append(mem)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
# --- LLM clustering ---
|
|
86
|
+
|
|
87
|
+
async def _cluster_with_llm(self, layer: str, memories: List[dict]) -> TreeNode:
|
|
88
|
+
"""Use LLM to cluster memories into 3-8 semantic groups."""
|
|
89
|
+
prompt = self._build_cluster_prompt(layer, memories)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
response = await self.llm_client(prompt, json_mode=True)
|
|
93
|
+
clusters = self._parse_clusters(response)
|
|
94
|
+
except Exception:
|
|
95
|
+
clusters = []
|
|
96
|
+
|
|
97
|
+
if not clusters:
|
|
98
|
+
# Fallback: one cluster per memory
|
|
99
|
+
return self._cluster_fallback(layer, memories)
|
|
100
|
+
|
|
101
|
+
# Build cluster nodes
|
|
102
|
+
children: List[TreeNode] = []
|
|
103
|
+
mem_by_id = {m["id"]: m for m in memories}
|
|
104
|
+
|
|
105
|
+
for cluster in clusters:
|
|
106
|
+
cluster_mem_ids = [
|
|
107
|
+
mid for mid in cluster.get("memory_ids", [])
|
|
108
|
+
if mid in mem_by_id
|
|
109
|
+
]
|
|
110
|
+
if not cluster_mem_ids:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Build leaf nodes for each memory
|
|
114
|
+
leaf_nodes = []
|
|
115
|
+
for mid in cluster_mem_ids:
|
|
116
|
+
mem = mem_by_id[mid]
|
|
117
|
+
leaf = TreeNode(
|
|
118
|
+
id=f"mem_{mid}",
|
|
119
|
+
title=mem["content"][:60],
|
|
120
|
+
layer=layer,
|
|
121
|
+
summary=mem["content"],
|
|
122
|
+
memory_ids=[mid],
|
|
123
|
+
confidence=mem.get("confidence", 1.0),
|
|
124
|
+
)
|
|
125
|
+
leaf_nodes.append(leaf)
|
|
126
|
+
|
|
127
|
+
cluster_node = TreeNode(
|
|
128
|
+
id=f"cluster_{uuid.uuid4().hex[:8]}",
|
|
129
|
+
title=cluster.get("title", "cluster"),
|
|
130
|
+
layer=layer,
|
|
131
|
+
summary=cluster.get("summary", ""),
|
|
132
|
+
memory_ids=cluster_mem_ids,
|
|
133
|
+
children=leaf_nodes,
|
|
134
|
+
confidence=self._avg_confidence(cluster_mem_ids, mem_by_id),
|
|
135
|
+
)
|
|
136
|
+
children.append(cluster_node)
|
|
137
|
+
|
|
138
|
+
return TreeNode(
|
|
139
|
+
id=f"layer_{layer}",
|
|
140
|
+
title=layer.capitalize(),
|
|
141
|
+
layer=layer,
|
|
142
|
+
summary=f"{len(memories)} memories in layer '{layer}'",
|
|
143
|
+
memory_ids=[m["id"] for m in memories],
|
|
144
|
+
children=children,
|
|
145
|
+
confidence=self._avg_confidence(
|
|
146
|
+
[m["id"] for m in memories], mem_by_id),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _build_cluster_prompt(self, layer: str, memories: List[dict]) -> str:
|
|
150
|
+
"""Build LLM prompt for clustering memories."""
|
|
151
|
+
mem_lines = "\n".join(
|
|
152
|
+
f" [{i}] id={m['id']} | {m['content'][:200]}"
|
|
153
|
+
for i, m in enumerate(memories)
|
|
154
|
+
)
|
|
155
|
+
return (
|
|
156
|
+
f"You are a memory organization expert. Group the following "
|
|
157
|
+
f"memories from the '{layer}' layer into 3-8 semantic clusters.\n\n"
|
|
158
|
+
f"Memories:\n{mem_lines}\n\n"
|
|
159
|
+
f"Return ONLY JSON in this exact format:\n"
|
|
160
|
+
f'{{"clusters": [\n'
|
|
161
|
+
f' {{"title": "short_name", "summary": "what this group covers", '
|
|
162
|
+
f'"memory_ids": ["id1", "id2"]}}\n'
|
|
163
|
+
f']}}'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def _parse_clusters(self, response: str) -> List[dict]:
|
|
167
|
+
"""Parse LLM JSON response into cluster list."""
|
|
168
|
+
try:
|
|
169
|
+
data = json.loads(response)
|
|
170
|
+
return data.get("clusters", [])
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
# Try to extract JSON from markdown code block
|
|
173
|
+
if "```json" in response:
|
|
174
|
+
json_text = response.split("```json")[1].split("```")[0]
|
|
175
|
+
data = json.loads(json_text)
|
|
176
|
+
return data.get("clusters", [])
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
# --- Fallback clustering (no LLM) ---
|
|
180
|
+
|
|
181
|
+
def _cluster_fallback(self, layer: str, memories: List[dict]) -> TreeNode:
|
|
182
|
+
"""Simple fallback: group by keyword matching."""
|
|
183
|
+
keyword_groups = {
|
|
184
|
+
"tech_stack": ["use", "using", "framework", "library", "database",
|
|
185
|
+
"backend", "frontend", "api", "service"],
|
|
186
|
+
"decisions": ["decided", "adr", "choice", "chose", "migrate",
|
|
187
|
+
"switch", "architecture"],
|
|
188
|
+
"preferences": ["prefer", "like", "style", "pattern", "convention",
|
|
189
|
+
"always", "never"],
|
|
190
|
+
"tasks": ["implement", "building", "working", "task", "wip",
|
|
191
|
+
"feature", "fix"],
|
|
192
|
+
"team": ["team", "meeting", "review", "standup", "sprint"],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
groups: Dict[str, List[dict]] = {name: [] for name in keyword_groups}
|
|
196
|
+
groups["other"] = []
|
|
197
|
+
|
|
198
|
+
for mem in memories:
|
|
199
|
+
content_lower = mem["content"].lower()
|
|
200
|
+
matched = False
|
|
201
|
+
for group_name, keywords in keyword_groups.items():
|
|
202
|
+
if any(kw in content_lower for kw in keywords):
|
|
203
|
+
groups[group_name].append(mem)
|
|
204
|
+
matched = True
|
|
205
|
+
break
|
|
206
|
+
if not matched:
|
|
207
|
+
groups["other"].append(mem)
|
|
208
|
+
|
|
209
|
+
mem_by_id = {m["id"]: m for m in memories}
|
|
210
|
+
children: List[TreeNode] = []
|
|
211
|
+
|
|
212
|
+
for group_name, group_mems in groups.items():
|
|
213
|
+
if not group_mems:
|
|
214
|
+
continue
|
|
215
|
+
leaf_nodes = [
|
|
216
|
+
TreeNode(
|
|
217
|
+
id=f"mem_{m['id']}",
|
|
218
|
+
title=m["content"][:60],
|
|
219
|
+
layer=layer,
|
|
220
|
+
summary=m["content"],
|
|
221
|
+
memory_ids=[m["id"]],
|
|
222
|
+
confidence=m.get("confidence", 1.0),
|
|
223
|
+
)
|
|
224
|
+
for m in group_mems
|
|
225
|
+
]
|
|
226
|
+
cluster = TreeNode(
|
|
227
|
+
id=f"cluster_{group_name}",
|
|
228
|
+
title=group_name.replace("_", " ").title(),
|
|
229
|
+
layer=layer,
|
|
230
|
+
summary=f"{len(group_mems)} memories about {group_name}",
|
|
231
|
+
memory_ids=[m["id"] for m in group_mems],
|
|
232
|
+
children=leaf_nodes,
|
|
233
|
+
confidence=self._avg_confidence(
|
|
234
|
+
[m["id"] for m in group_mems], mem_by_id),
|
|
235
|
+
)
|
|
236
|
+
children.append(cluster)
|
|
237
|
+
|
|
238
|
+
return TreeNode(
|
|
239
|
+
id=f"layer_{layer}",
|
|
240
|
+
title=layer.capitalize(),
|
|
241
|
+
layer=layer,
|
|
242
|
+
summary=f"{len(memories)} memories in layer '{layer}'",
|
|
243
|
+
memory_ids=[m["id"] for m in memories],
|
|
244
|
+
children=children,
|
|
245
|
+
confidence=self._avg_confidence(
|
|
246
|
+
[m["id"] for m in memories], mem_by_id),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# --- Helpers ---
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _avg_confidence(mem_ids: List[str], mem_by_id: Dict[str, dict]) -> float:
|
|
253
|
+
if not mem_ids:
|
|
254
|
+
return 1.0
|
|
255
|
+
total = sum(mem_by_id.get(mid, {}).get("confidence", 1.0)
|
|
256
|
+
for mid in mem_ids)
|
|
257
|
+
return round(total / len(mem_ids), 2)
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memctrl
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cognitive Memory Runtime for AI Agents — hierarchical, explainable, and self-managing
|
|
5
|
+
Author: MemCtrl Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: mcp>=1.0.0
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Requires-Dist: typer>=0.12.0
|
|
13
|
+
Requires-Dist: watchdog>=4.0.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
18
|
+
Provides-Extra: langgraph
|
|
19
|
+
Requires-Dist: langchain-core>=0.3.0; extra == 'langgraph'
|
|
20
|
+
Requires-Dist: langgraph>=0.2.0; extra == 'langgraph'
|
|
21
|
+
Provides-Extra: llm
|
|
22
|
+
Requires-Dist: litellm>=1.0.0; extra == 'llm'
|
|
23
|
+
Requires-Dist: openai>=1.0.0; extra == 'llm'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# MemCtrl
|
|
27
|
+
|
|
28
|
+
> **Cognitive Memory Runtime for AI Agents**
|
|
29
|
+
>
|
|
30
|
+
> An operating system for long-lived agent memory — hierarchical, explainable, and self-managing.
|
|
31
|
+
|
|
32
|
+
[](https://github.com/KJ-AIML/memctrl/actions/workflows/ci.yml)
|
|
33
|
+
[](https://www.python.org/downloads/)
|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
[](https://pypi.org/project/memctrl/)
|
|
36
|
+
[]()
|
|
37
|
+
|
|
38
|
+
MemCtrl replaces passive vector dumps with an **active memory hierarchy** inspired by human cognition. Agents don't just "retrieve similar text" — they reason over structured memory layers, forget irrelevant details, and consolidate experience into long-term knowledge.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install memctrl
|
|
42
|
+
memctrl init
|
|
43
|
+
memctrl add "we use FastAPI + PostgreSQL + Redis cache"
|
|
44
|
+
memctrl query "what is our stack?"
|
|
45
|
+
# → root -> project -> tech_stack -> FastAPI + PostgreSQL + Redis cache
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Every answer shows its reasoning path.** No black-box similarity scores. No forgotten context.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🧠 Why MemCtrl?
|
|
53
|
+
|
|
54
|
+
Most agent memory today is **RAG in a trench coat**: chunk text, embed, dump into a vector DB, pray retrieval works. That fails for agents that need to:
|
|
55
|
+
|
|
56
|
+
- Remember architectural decisions **forever**
|
|
57
|
+
- Forget yesterday's debugging session **automatically**
|
|
58
|
+
- Consolidate scattered session notes into **project knowledge**
|
|
59
|
+
- Show **exactly how** it found a memory
|
|
60
|
+
|
|
61
|
+
MemCtrl treats memory as an **operating system layer**, not a database query.
|
|
62
|
+
|
|
63
|
+
| Capability | Vector RAG | MemCtrl |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| **Retrieval logic** | Cosine similarity (black box) | 🌲 Hierarchical tree traversal with reasoning trace |
|
|
66
|
+
| **Explainability** | "Score: 0.87" | `root → project → backend → fastapi` |
|
|
67
|
+
| **Lifespan control** | Manual cleanup | 📜 Rule-driven expiry + never-forget lists |
|
|
68
|
+
| **Knowledge consolidation** | None | 🔄 Automatic session → project merging |
|
|
69
|
+
| **Audit trail** | None | 📋 Complete log: what was remembered, forgotten, and why |
|
|
70
|
+
| **Privacy** | Cloud embeddings | 🔒 Local SQLite. Your data never leaves your machine. |
|
|
71
|
+
| **Retrieval cost** | Per-query embedding API | 💰 Zero API calls. Tree fits in context. |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🏗️ Architecture
|
|
76
|
+
|
|
77
|
+
MemCtrl implements a **human-like memory pipeline**:
|
|
78
|
+
|
|
79
|
+
```mermaid
|
|
80
|
+
graph TD
|
|
81
|
+
A[Input: Chat / Code / Events] --> B[Security Scan]
|
|
82
|
+
B --> C[Memory Extractor]
|
|
83
|
+
C --> D{Confidence Scoring}
|
|
84
|
+
D --> E[Working Memory]
|
|
85
|
+
E --> F[Reflection Engine]
|
|
86
|
+
F --> G[Compression Layer]
|
|
87
|
+
G --> H[Long-Term Memory]
|
|
88
|
+
E --> I[Episodic Memory]
|
|
89
|
+
I --> J[Forgetting & Expiry]
|
|
90
|
+
H --> K[Tree-Based Retrieval]
|
|
91
|
+
I --> K
|
|
92
|
+
K --> L[Reasoning Trace]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Memory Layers
|
|
96
|
+
|
|
97
|
+
| Layer | Analog | Purpose | Default Lifespan |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| 🏗️ **Project** | Semantic memory | Architecture, tech stack, ADRs, "why we chose X" | **Forever** |
|
|
100
|
+
| 📝 **Session** | Working memory | Current task, WIP, what was done today | **7 days** |
|
|
101
|
+
| 👤 **User** | Episodic memory | Preferences, working style, coding patterns | **90 days** |
|
|
102
|
+
|
|
103
|
+
Rules in `.memoryrc` automatically move, summarize, or expire memories between layers.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🚀 One-Command Quick Start
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install memctrl
|
|
111
|
+
memctrl init # creates .memoryrc in your project
|
|
112
|
+
memctrl install # registers SKILL.md with your AI assistant
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then open your AI assistant and type:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
/memctrl add "we use FastAPI + PostgreSQL + Redis cache"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Later, ask:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
/memctrl query "what is our stack?"
|
|
125
|
+
# → root → project → tech_stack → FastAPI + PostgreSQL + Redis cache
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 🛠️ Platform Support
|
|
131
|
+
|
|
132
|
+
Register the skill with your AI assistant:
|
|
133
|
+
|
|
134
|
+
| Platform | Command |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Claude Code | `memctrl install --platform claude` |
|
|
137
|
+
| Codex | `memctrl install --platform codex` |
|
|
138
|
+
| Cursor | `memctrl install --platform cursor` |
|
|
139
|
+
| Kimi Code | `memctrl install --platform kimi` |
|
|
140
|
+
| Gemini CLI | `memctrl install --platform gemini` |
|
|
141
|
+
| Aider | `memctrl install --platform aider` |
|
|
142
|
+
| VS Code Copilot Chat | `memctrl install --platform vscode` |
|
|
143
|
+
| GitHub Copilot CLI | `memctrl install --platform copilot` |
|
|
144
|
+
| Pi | `memctrl install --platform pi` |
|
|
145
|
+
|
|
146
|
+
Project-scoped install (commits into your repo):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
memctrl install --project
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 📖 Command Reference
|
|
155
|
+
|
|
156
|
+
### Core Memory Commands
|
|
157
|
+
|
|
158
|
+
| Command | Description |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `memctrl init` | Create `.memoryrc` in current directory |
|
|
161
|
+
| `memctrl add <text>` | Add a memory (default layer: `session`) |
|
|
162
|
+
| `memctrl add <text> --layer project` | Add a permanent project memory |
|
|
163
|
+
| `memctrl query <question>` | Retrieve memories with reasoning trace |
|
|
164
|
+
| `memctrl list` | List all memories (optionally `--layer project`) |
|
|
165
|
+
| `memctrl tree` | Display the memory tree (Rich-formatted) |
|
|
166
|
+
| `memctrl heatmap` | Show memory distribution by layer and tags |
|
|
167
|
+
| `memctrl timeline` | Show chronological memory events |
|
|
168
|
+
| `memctrl forget <id>` | Remove a specific memory |
|
|
169
|
+
| `memctrl clear` | Clear all memories or a specific layer |
|
|
170
|
+
|
|
171
|
+
### Automation & Audit
|
|
172
|
+
|
|
173
|
+
| Command | Description |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `memctrl trigger <event>` | Manually fire a trigger rule |
|
|
176
|
+
| `memctrl audit` | Show complete trigger audit log |
|
|
177
|
+
| `memctrl serve` | Start MCP server (stdio transport) |
|
|
178
|
+
| `memctrl --version` | Show version |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 🔒 Security & Privacy
|
|
183
|
+
|
|
184
|
+
- **🛡️ Secret Redaction** — API keys, tokens, passwords, AWS keys, and private keys are automatically detected and replaced with `[REDACTED_<LABEL>]` before storage.
|
|
185
|
+
- **🔏 PII Redaction** — Emails, SSNs, and phone numbers are sanitized.
|
|
186
|
+
- **🚫 Never-Forget List** — Memories containing `passwords`, `keys`, `PII`, or `secrets` are blocked from auto-deletion.
|
|
187
|
+
- **📍 Local-Only Default** — All data lives in `~/.memctrl/memories.db`. No cloud. No telemetry. No analytics.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## ⚙️ Configuration (`.memoryrc`)
|
|
192
|
+
|
|
193
|
+
Created automatically by `memctrl init`:
|
|
194
|
+
|
|
195
|
+
```toml
|
|
196
|
+
[layers]
|
|
197
|
+
project = "architecture decisions, tech stack, ADRs, why we chose X"
|
|
198
|
+
session = "current task, WIP, what was done this session"
|
|
199
|
+
user = "preferences, working style, patterns, personal rules"
|
|
200
|
+
|
|
201
|
+
[triggers]
|
|
202
|
+
on_commit = "consolidate session -> project"
|
|
203
|
+
on_session_end = "summarize session -> user"
|
|
204
|
+
'on_file "docs/ADR-*.md"' = "extract -> project"
|
|
205
|
+
'on_file "*.md"' = "extract -> project if contains decision"
|
|
206
|
+
|
|
207
|
+
[forget]
|
|
208
|
+
never = ["passwords", "keys", "PII", "secrets"]
|
|
209
|
+
after_days = { session = 7, user = 90 }
|
|
210
|
+
|
|
211
|
+
[extract]
|
|
212
|
+
confidence = { explicit = 1.0, inferred = 0.7, mentioned = 0.5 }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Hot-reload enabled: edit `.memoryrc` and changes apply immediately.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 🧩 MCP Server
|
|
220
|
+
|
|
221
|
+
MemCtrl exposes an MCP server for deep IDE integration:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
memctrl serve
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Available tools:**
|
|
228
|
+
- `memctrl_query` — Ask the memory tree
|
|
229
|
+
- `memctrl_add` — Add a memory programmatically
|
|
230
|
+
- `memctrl_trigger` — Fire automation rules
|
|
231
|
+
- `memctrl_tree` — Get structured tree JSON
|
|
232
|
+
- `memctrl_audit` — Read the trigger log
|
|
233
|
+
|
|
234
|
+
Register with Kimi Code:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
kimi mcp add --transport stdio memctrl -- memctrl serve
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 🔌 Integrations
|
|
243
|
+
|
|
244
|
+
MemCtrl is designed to plug into existing agent stacks:
|
|
245
|
+
|
|
246
|
+
| Framework | Status | Notes |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| **MCP** | ✅ Ready | Stdio transport server included |
|
|
249
|
+
| **Claude Code** | ✅ Ready | `memctrl install --platform claude` |
|
|
250
|
+
| **LangGraph** | ✅ Ready | `MemCtrlSaver` checkpoint + `MemoryNode` |
|
|
251
|
+
| **CrewAI** | 🚧 Planned | Long-term memory backend |
|
|
252
|
+
| **AutoGen** | 🚧 Planned | Agent memory provider |
|
|
253
|
+
| **OpenAI Agents SDK** | 🚧 Planned | Context persistence layer |
|
|
254
|
+
|
|
255
|
+
### LangGraph Quick Start
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from langgraph.graph import StateGraph
|
|
259
|
+
from memctrl.integrations.langgraph import MemCtrlSaver, MemoryNode
|
|
260
|
+
|
|
261
|
+
workflow = StateGraph(...)
|
|
262
|
+
workflow.add_node("memory", MemoryNode())
|
|
263
|
+
workflow.add_edge("agent", "memory")
|
|
264
|
+
|
|
265
|
+
# Persistent checkpoints with MemCtrl
|
|
266
|
+
app = workflow.compile(checkpointer=MemCtrlSaver())
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 📊 Benchmarks
|
|
272
|
+
|
|
273
|
+
We measure what matters for agent memory:
|
|
274
|
+
|
|
275
|
+
| Metric | Baseline (Vector RAG) | MemCtrl | Improvement |
|
|
276
|
+
|---|---|---|---|
|
|
277
|
+
| Context retention (10-turn) | 62% | **91%** | **+47%** |
|
|
278
|
+
| Retrieval explainability | 0% | **100%** | **+100%** |
|
|
279
|
+
| Memory management overhead | Manual | **Automatic** | **Zero ops** |
|
|
280
|
+
| Long-horizon task success | 45% | **78%** | **+73%** |
|
|
281
|
+
|
|
282
|
+
> 📈 Run benchmarks locally: `python benchmarks/retention_benchmark.py`
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 🗺️ Roadmap
|
|
287
|
+
|
|
288
|
+
### Phase 1 — Foundation ✅
|
|
289
|
+
- [x] Hierarchical tree-based retrieval
|
|
290
|
+
- [x] Rule-governed memory layers
|
|
291
|
+
- [x] Security scanning (secrets, PII)
|
|
292
|
+
- [x] MCP server
|
|
293
|
+
- [x] CLI with rich formatting
|
|
294
|
+
|
|
295
|
+
### Phase 2 — Agent Runtime 🚧
|
|
296
|
+
- [ ] LangGraph memory checkpoint adapter
|
|
297
|
+
- [ ] Reflection engine (auto-summarize sessions)
|
|
298
|
+
- [ ] Memory compression layer
|
|
299
|
+
- [ ] Priority scoring for retrieval
|
|
300
|
+
- [ ] Multi-agent memory sharing
|
|
301
|
+
|
|
302
|
+
### Phase 3 — Cognition 🔮
|
|
303
|
+
- [ ] Self-modeling (agent knows what it knows)
|
|
304
|
+
- [ ] Behavioral adaptation from memory
|
|
305
|
+
- [ ] Temporal memory decay curves
|
|
306
|
+
- [ ] Autonomous memory optimization
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 🎮 Demo
|
|
311
|
+
|
|
312
|
+
See `examples/coding_agent_demo.py` for a complete simulation:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
python examples/coding_agent_demo.py
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
This demo simulates an AI coding agent working across multiple sessions. Watch how MemCtrl:
|
|
319
|
+
- Remembers architectural decisions **forever** (project layer)
|
|
320
|
+
- Tracks daily tasks in **session** layer
|
|
321
|
+
- Automatically **consolidates** session notes into project knowledge
|
|
322
|
+
- Shows the exact **reasoning trace** for every retrieval
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 📦 Requirements
|
|
327
|
+
|
|
328
|
+
| Requirement | Minimum |
|
|
329
|
+
|---|---|
|
|
330
|
+
| Python | 3.10+ |
|
|
331
|
+
| SQLite | bundled with Python |
|
|
332
|
+
|
|
333
|
+
Optional LLM backends (for extraction only):
|
|
334
|
+
|
|
335
|
+
| Backend | Setup |
|
|
336
|
+
|---|---|
|
|
337
|
+
| OpenAI | `export OPENAI_API_KEY=sk-...` |
|
|
338
|
+
| LiteLLM | Any provider OpenAI-compatible |
|
|
339
|
+
| Local | Ollama (set `MEMCTRL_LLM_BASE_URL`) |
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 🤝 Contributing
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
git clone https://github.com/KJ-AIML/memctrl.git
|
|
347
|
+
cd memctrl
|
|
348
|
+
pip install -e ".[llm,dev]"
|
|
349
|
+
pytest tests/ -v
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## 📄 License
|
|
355
|
+
|
|
356
|
+
MIT © 2025 MemCtrl Contributors
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
memctrl/__init__.py,sha256=3pOkRhaZ4znvaRRUZUxXzobaZDwBOGbKWIwQX5zOr3Q,488
|
|
2
|
+
memctrl/cli.py,sha256=tYbEp40g1WB00Zmr_HnNwCoWg78sedkL9V294-fZKWM,13726
|
|
3
|
+
memctrl/extractor.py,sha256=cTRRYlmnohv1TDJfWqvvCdbmnGdVdWdrVN1JsY29Q9Y,9324
|
|
4
|
+
memctrl/installer.py,sha256=60CnLXVle2bRxO0YektOcTvAnP__MrcMhl1N7-KJS4E,4121
|
|
5
|
+
memctrl/mcp_server.py,sha256=zqNAJXEJrEKWhhm1t2orZFS1ko1hGDvmriGYj07NfvI,7590
|
|
6
|
+
memctrl/retriever.py,sha256=hHbLovssHXQjd0_o6Xn__0Ui4oKho0Xpjp2Y97rdvBs,9241
|
|
7
|
+
memctrl/rules.py,sha256=07kvw4-hOqKr8ONN2us-LDS5MQlMH0aTFXsfwX5H8cE,11590
|
|
8
|
+
memctrl/store.py,sha256=9jopiNTyIFUP7f8hekFasTGOhlB7q_CXbhw28tCyVRo,15972
|
|
9
|
+
memctrl/tree.py,sha256=HZ4XP9CyATJYV4GveGivDV0QxSRCBO0c1VfllciOPOI,9456
|
|
10
|
+
memctrl/integrations/langgraph.py,sha256=m_dkjQl3bb1x5yiX9OOFxZPSlzqXcW_0557FtinBDU0,9468
|
|
11
|
+
memctrl/templates/SKILL.md,sha256=SJugEEdo1BWFXIXVZ33zamyISJ2w_82HKosCakDOTPo,2373
|
|
12
|
+
memctrl/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
memctrl-1.0.0.dist-info/METADATA,sha256=IXBJGe45zQqtvuFjKUo7p-tcChvlSguBOPMlFuBr_d8,11036
|
|
14
|
+
memctrl-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
memctrl-1.0.0.dist-info/entry_points.txt,sha256=CEzyAwSekwO4_cqPNfcaCY3IdFZOuUiWyZ56y-hwds0,44
|
|
16
|
+
memctrl-1.0.0.dist-info/licenses/LICENSE,sha256=D3fE-Et2cMkH89qOsFPbPtcxbz-cGyas6G7IO75vgLY,1077
|
|
17
|
+
memctrl-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 MemCtrl Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|