openmem-engine 0.1.0__tar.gz
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.
- openmem_engine-0.1.0/.github/workflows/docs.yml +46 -0
- openmem_engine-0.1.0/.github/workflows/publish.yml +28 -0
- openmem_engine-0.1.0/.gitignore +10 -0
- openmem_engine-0.1.0/PKG-INFO +166 -0
- openmem_engine-0.1.0/README.md +139 -0
- openmem_engine-0.1.0/plugin/.claude-plugin/plugin.json +9 -0
- openmem_engine-0.1.0/plugin/.mcp.json +9 -0
- openmem_engine-0.1.0/plugin/servers/formatting.py +69 -0
- openmem_engine-0.1.0/plugin/servers/openmem_server.py +209 -0
- openmem_engine-0.1.0/plugin/skills/recall/SKILL.md +31 -0
- openmem_engine-0.1.0/plugin/skills/status/SKILL.md +25 -0
- openmem_engine-0.1.0/plugin/skills/store/SKILL.md +41 -0
- openmem_engine-0.1.0/pyproject.toml +41 -0
- openmem_engine-0.1.0/src/openmem/__init__.py +4 -0
- openmem_engine-0.1.0/src/openmem/activation.py +35 -0
- openmem_engine-0.1.0/src/openmem/conflict.py +59 -0
- openmem_engine-0.1.0/src/openmem/engine.py +182 -0
- openmem_engine-0.1.0/src/openmem/models.py +40 -0
- openmem_engine-0.1.0/src/openmem/scoring.py +118 -0
- openmem_engine-0.1.0/src/openmem/store.py +206 -0
- openmem_engine-0.1.0/tests/test_activation.py +63 -0
- openmem_engine-0.1.0/tests/test_engine.py +160 -0
- openmem_engine-0.1.0/tests/test_scoring.py +101 -0
- openmem_engine-0.1.0/tests/test_store.py +141 -0
- openmem_engine-0.1.0/uv.lock +861 -0
- openmem_engine-0.1.0/web/.gitignore +20 -0
- openmem_engine-0.1.0/web/README.md +41 -0
- openmem_engine-0.1.0/web/docs/api.md +267 -0
- openmem_engine-0.1.0/web/docs/claude-code.md +171 -0
- openmem_engine-0.1.0/web/docs/configuration.md +129 -0
- openmem_engine-0.1.0/web/docs/integration.md +187 -0
- openmem_engine-0.1.0/web/docs/intro.md +60 -0
- openmem_engine-0.1.0/web/docs/quickstart.md +117 -0
- openmem_engine-0.1.0/web/docs/retrieval.md +173 -0
- openmem_engine-0.1.0/web/docusaurus.config.ts +99 -0
- openmem_engine-0.1.0/web/package-lock.json +19706 -0
- openmem_engine-0.1.0/web/package.json +48 -0
- openmem_engine-0.1.0/web/sidebars.ts +15 -0
- openmem_engine-0.1.0/web/src/css/custom.css +22 -0
- openmem_engine-0.1.0/web/src/pages/index.module.css +25 -0
- openmem_engine-0.1.0/web/src/pages/index.tsx +72 -0
- openmem_engine-0.1.0/web/static/.nojekyll +0 -0
- openmem_engine-0.1.0/web/static/img/docusaurus-social-card.jpg +0 -0
- openmem_engine-0.1.0/web/static/img/docusaurus.png +0 -0
- openmem_engine-0.1.0/web/static/img/favicon.ico +0 -0
- openmem_engine-0.1.0/web/static/img/logo.svg +1 -0
- openmem_engine-0.1.0/web/static/img/undraw_docusaurus_mountain.svg +171 -0
- openmem_engine-0.1.0/web/static/img/undraw_docusaurus_react.svg +170 -0
- openmem_engine-0.1.0/web/static/img/undraw_docusaurus_tree.svg +40 -0
- openmem_engine-0.1.0/web/tsconfig.json +8 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Deploy docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "web/**"
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pages: write
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: pages
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
deploy:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment:
|
|
23
|
+
name: github-pages
|
|
24
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
25
|
+
defaults:
|
|
26
|
+
run:
|
|
27
|
+
working-directory: web
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- uses: actions/setup-node@v4
|
|
32
|
+
with:
|
|
33
|
+
node-version: 20
|
|
34
|
+
cache: npm
|
|
35
|
+
cache-dependency-path: web/package-lock.json
|
|
36
|
+
|
|
37
|
+
- run: npm ci
|
|
38
|
+
|
|
39
|
+
- run: npm run build
|
|
40
|
+
|
|
41
|
+
- uses: actions/upload-pages-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
path: web/build
|
|
44
|
+
|
|
45
|
+
- id: deployment
|
|
46
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install build tools
|
|
22
|
+
run: pip install build
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: python -m build
|
|
26
|
+
|
|
27
|
+
- name: Publish to PyPI
|
|
28
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openmem-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cognitive memory engine for AI agents — human-inspired retrieval via activation, competition, and reconstruction
|
|
5
|
+
Project-URL: Homepage, https://github.com/dunkinfrunkin/OpenMem
|
|
6
|
+
Project-URL: Documentation, https://dunkinfrunkin.github.io/OpenMem/
|
|
7
|
+
Project-URL: Repository, https://github.com/dunkinfrunkin/OpenMem
|
|
8
|
+
Project-URL: Issues, https://github.com/dunkinfrunkin/OpenMem/issues
|
|
9
|
+
Author: OpenMem
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: agents,ai,cognitive,llm,memory,sqlite
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: mcp>=1.0; extra == 'mcp'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# OpenMem
|
|
29
|
+
|
|
30
|
+
Deterministic memory engine for AI agents. Retrieves context via BM25 lexical search, graph-based spreading activation, and human-inspired competition scoring. SQLite-backed, zero dependencies.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Query → FTS5/BM25 (lexical trigger)
|
|
36
|
+
→ Seed Activation
|
|
37
|
+
→ Spreading Activation (graph edges, max 2 hops)
|
|
38
|
+
→ Recency + Strength + Confidence weighting
|
|
39
|
+
→ Competition (score-based ranking)
|
|
40
|
+
→ Context Pack (token-budgeted output)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
No vectors, no embeddings, no LLM in the retrieval loop. The LLM is the consumer, not the retriever.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install openmem-engine
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or from source:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/yourorg/openmem.git
|
|
55
|
+
cd openmem
|
|
56
|
+
pip install -e ".[dev]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from openmem import MemoryEngine
|
|
63
|
+
|
|
64
|
+
engine = MemoryEngine() # in-memory, or MemoryEngine("memories.db") for persistence
|
|
65
|
+
|
|
66
|
+
# Store memories
|
|
67
|
+
m1 = engine.add("We chose SQLite over Postgres for simplicity", type="decision", entities=["SQLite", "Postgres"])
|
|
68
|
+
m2 = engine.add("Postgres has better concurrent write support", type="fact", entities=["Postgres"])
|
|
69
|
+
|
|
70
|
+
# Link related memories
|
|
71
|
+
engine.link(m1.id, m2.id, "supports")
|
|
72
|
+
|
|
73
|
+
# Recall
|
|
74
|
+
results = engine.recall("Why did we pick SQLite?")
|
|
75
|
+
for r in results:
|
|
76
|
+
print(f"{r.score:.3f} | {r.memory.text}")
|
|
77
|
+
# 0.800 | We chose SQLite over Postgres for simplicity
|
|
78
|
+
# 0.500 | Postgres has better concurrent write support
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Claude Code plugin
|
|
82
|
+
|
|
83
|
+
Install OpenMem as a Claude Code plugin to get persistent memory across sessions:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install openmem-engine "mcp>=1.0"
|
|
87
|
+
claude plugin install --path ./plugin
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Once installed, you get three slash commands:
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
|---------|-------------|
|
|
94
|
+
| `/openmem:recall` | Recall memories relevant to the current conversation |
|
|
95
|
+
| `/openmem:store` | Store key facts, decisions, and preferences from the conversation |
|
|
96
|
+
| `/openmem:status` | Show memory store statistics |
|
|
97
|
+
|
|
98
|
+
The plugin also registers an MCP server with 7 tools (`memory_store`, `memory_recall`, `memory_link`, `memory_reinforce`, `memory_supersede`, `memory_contradict`, `memory_stats`) that Claude can call automatically.
|
|
99
|
+
|
|
100
|
+
Memories persist in `~/.openmem/memories.db` by default (override with the `OPENMEM_DB` env var).
|
|
101
|
+
|
|
102
|
+
## Usage with an LLM agent
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
engine = MemoryEngine("project.db")
|
|
106
|
+
|
|
107
|
+
# Agent stores what it learns
|
|
108
|
+
engine.add("User prefers TypeScript over JavaScript", type="preference", entities=["TypeScript", "JavaScript"])
|
|
109
|
+
engine.add("Auth system uses JWT with 24h expiry", type="decision", entities=["JWT", "auth"])
|
|
110
|
+
engine.add("The /api/users endpoint returns 500 on empty payload", type="incident", entities=["/api/users"])
|
|
111
|
+
|
|
112
|
+
# Before each LLM call, recall relevant context
|
|
113
|
+
results = engine.recall("set up authentication", top_k=5, token_budget=2000)
|
|
114
|
+
context = "\n".join(r.memory.text for r in results)
|
|
115
|
+
|
|
116
|
+
prompt = f"""Relevant context from previous work:
|
|
117
|
+
{context}
|
|
118
|
+
|
|
119
|
+
User request: {user_message}"""
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
### `MemoryEngine(db_path=":memory:", **config)`
|
|
125
|
+
|
|
126
|
+
| Method | Description |
|
|
127
|
+
|--------|-------------|
|
|
128
|
+
| `add(text, type="fact", entities=None, confidence=1.0, gist=None)` | Store a memory |
|
|
129
|
+
| `link(source_id, target_id, rel_type, weight=0.5)` | Create an edge between memories |
|
|
130
|
+
| `recall(query, top_k=5, token_budget=2000)` | Retrieve relevant memories |
|
|
131
|
+
| `reinforce(memory_id)` | Boost a memory's strength |
|
|
132
|
+
| `supersede(old_id, new_id)` | Mark a memory as outdated |
|
|
133
|
+
| `contradict(id_a, id_b)` | Flag two memories as contradicting |
|
|
134
|
+
| `decay_all()` | Run decay pass over all memories |
|
|
135
|
+
| `stats()` | Get summary statistics |
|
|
136
|
+
|
|
137
|
+
### Memory types
|
|
138
|
+
|
|
139
|
+
`fact` · `decision` · `preference` · `incident` · `plan` · `constraint`
|
|
140
|
+
|
|
141
|
+
### Edge types
|
|
142
|
+
|
|
143
|
+
`mentions` · `supports` · `contradicts` · `depends_on` · `same_as`
|
|
144
|
+
|
|
145
|
+
## Retrieval model
|
|
146
|
+
|
|
147
|
+
**Recency** — Exponential decay with ~14-day half-life. Recently accessed memories surface first.
|
|
148
|
+
|
|
149
|
+
**Strength** — Reinforced on access, decays naturally over time. Frequently recalled memories persist.
|
|
150
|
+
|
|
151
|
+
**Spreading activation** — Memories linked by edges activate their neighbors. A query hitting one memory pulls in related context up to 2 hops away.
|
|
152
|
+
|
|
153
|
+
**Competition** — Final score combines activation (50%), recency (20%), strength (20%), and confidence (10%). Superseded memories are penalized 50%, contradicted ones 70%.
|
|
154
|
+
|
|
155
|
+
**Conflict resolution** — When two contradicting memories both activate, the weaker one (by strength × confidence × recency) gets demoted.
|
|
156
|
+
|
|
157
|
+
## Tests
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install -e ".[dev]"
|
|
161
|
+
pytest tests/ -v
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# OpenMem
|
|
2
|
+
|
|
3
|
+
Deterministic memory engine for AI agents. Retrieves context via BM25 lexical search, graph-based spreading activation, and human-inspired competition scoring. SQLite-backed, zero dependencies.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Query → FTS5/BM25 (lexical trigger)
|
|
9
|
+
→ Seed Activation
|
|
10
|
+
→ Spreading Activation (graph edges, max 2 hops)
|
|
11
|
+
→ Recency + Strength + Confidence weighting
|
|
12
|
+
→ Competition (score-based ranking)
|
|
13
|
+
→ Context Pack (token-budgeted output)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
No vectors, no embeddings, no LLM in the retrieval loop. The LLM is the consumer, not the retriever.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install openmem-engine
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or from source:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/yourorg/openmem.git
|
|
28
|
+
cd openmem
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from openmem import MemoryEngine
|
|
36
|
+
|
|
37
|
+
engine = MemoryEngine() # in-memory, or MemoryEngine("memories.db") for persistence
|
|
38
|
+
|
|
39
|
+
# Store memories
|
|
40
|
+
m1 = engine.add("We chose SQLite over Postgres for simplicity", type="decision", entities=["SQLite", "Postgres"])
|
|
41
|
+
m2 = engine.add("Postgres has better concurrent write support", type="fact", entities=["Postgres"])
|
|
42
|
+
|
|
43
|
+
# Link related memories
|
|
44
|
+
engine.link(m1.id, m2.id, "supports")
|
|
45
|
+
|
|
46
|
+
# Recall
|
|
47
|
+
results = engine.recall("Why did we pick SQLite?")
|
|
48
|
+
for r in results:
|
|
49
|
+
print(f"{r.score:.3f} | {r.memory.text}")
|
|
50
|
+
# 0.800 | We chose SQLite over Postgres for simplicity
|
|
51
|
+
# 0.500 | Postgres has better concurrent write support
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Claude Code plugin
|
|
55
|
+
|
|
56
|
+
Install OpenMem as a Claude Code plugin to get persistent memory across sessions:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install openmem-engine "mcp>=1.0"
|
|
60
|
+
claude plugin install --path ./plugin
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Once installed, you get three slash commands:
|
|
64
|
+
|
|
65
|
+
| Command | Description |
|
|
66
|
+
|---------|-------------|
|
|
67
|
+
| `/openmem:recall` | Recall memories relevant to the current conversation |
|
|
68
|
+
| `/openmem:store` | Store key facts, decisions, and preferences from the conversation |
|
|
69
|
+
| `/openmem:status` | Show memory store statistics |
|
|
70
|
+
|
|
71
|
+
The plugin also registers an MCP server with 7 tools (`memory_store`, `memory_recall`, `memory_link`, `memory_reinforce`, `memory_supersede`, `memory_contradict`, `memory_stats`) that Claude can call automatically.
|
|
72
|
+
|
|
73
|
+
Memories persist in `~/.openmem/memories.db` by default (override with the `OPENMEM_DB` env var).
|
|
74
|
+
|
|
75
|
+
## Usage with an LLM agent
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
engine = MemoryEngine("project.db")
|
|
79
|
+
|
|
80
|
+
# Agent stores what it learns
|
|
81
|
+
engine.add("User prefers TypeScript over JavaScript", type="preference", entities=["TypeScript", "JavaScript"])
|
|
82
|
+
engine.add("Auth system uses JWT with 24h expiry", type="decision", entities=["JWT", "auth"])
|
|
83
|
+
engine.add("The /api/users endpoint returns 500 on empty payload", type="incident", entities=["/api/users"])
|
|
84
|
+
|
|
85
|
+
# Before each LLM call, recall relevant context
|
|
86
|
+
results = engine.recall("set up authentication", top_k=5, token_budget=2000)
|
|
87
|
+
context = "\n".join(r.memory.text for r in results)
|
|
88
|
+
|
|
89
|
+
prompt = f"""Relevant context from previous work:
|
|
90
|
+
{context}
|
|
91
|
+
|
|
92
|
+
User request: {user_message}"""
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
### `MemoryEngine(db_path=":memory:", **config)`
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
|--------|-------------|
|
|
101
|
+
| `add(text, type="fact", entities=None, confidence=1.0, gist=None)` | Store a memory |
|
|
102
|
+
| `link(source_id, target_id, rel_type, weight=0.5)` | Create an edge between memories |
|
|
103
|
+
| `recall(query, top_k=5, token_budget=2000)` | Retrieve relevant memories |
|
|
104
|
+
| `reinforce(memory_id)` | Boost a memory's strength |
|
|
105
|
+
| `supersede(old_id, new_id)` | Mark a memory as outdated |
|
|
106
|
+
| `contradict(id_a, id_b)` | Flag two memories as contradicting |
|
|
107
|
+
| `decay_all()` | Run decay pass over all memories |
|
|
108
|
+
| `stats()` | Get summary statistics |
|
|
109
|
+
|
|
110
|
+
### Memory types
|
|
111
|
+
|
|
112
|
+
`fact` · `decision` · `preference` · `incident` · `plan` · `constraint`
|
|
113
|
+
|
|
114
|
+
### Edge types
|
|
115
|
+
|
|
116
|
+
`mentions` · `supports` · `contradicts` · `depends_on` · `same_as`
|
|
117
|
+
|
|
118
|
+
## Retrieval model
|
|
119
|
+
|
|
120
|
+
**Recency** — Exponential decay with ~14-day half-life. Recently accessed memories surface first.
|
|
121
|
+
|
|
122
|
+
**Strength** — Reinforced on access, decays naturally over time. Frequently recalled memories persist.
|
|
123
|
+
|
|
124
|
+
**Spreading activation** — Memories linked by edges activate their neighbors. A query hitting one memory pulls in related context up to 2 hops away.
|
|
125
|
+
|
|
126
|
+
**Competition** — Final score combines activation (50%), recency (20%), strength (20%), and confidence (10%). Superseded memories are penalized 50%, contradicted ones 70%.
|
|
127
|
+
|
|
128
|
+
**Conflict resolution** — When two contradicting memories both activate, the weaker one (by strength × confidence × recency) gets demoted.
|
|
129
|
+
|
|
130
|
+
## Tests
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pip install -e ".[dev]"
|
|
134
|
+
pytest tests/ -v
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Output formatting helpers for the OpenMem MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openmem.models import Memory, ScoredMemory
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_memory(mem: Memory) -> str:
|
|
9
|
+
"""Format a single Memory as structured plain text."""
|
|
10
|
+
lines = [
|
|
11
|
+
f"[{mem.id}]",
|
|
12
|
+
f" type: {mem.type}",
|
|
13
|
+
f" status: {mem.status}",
|
|
14
|
+
f" text: {mem.text}",
|
|
15
|
+
]
|
|
16
|
+
if mem.gist:
|
|
17
|
+
lines.append(f" gist: {mem.gist}")
|
|
18
|
+
if mem.entities:
|
|
19
|
+
lines.append(f" entities: {', '.join(mem.entities)}")
|
|
20
|
+
lines.append(f" strength: {mem.strength:.2f}")
|
|
21
|
+
lines.append(f" confidence: {mem.confidence:.2f}")
|
|
22
|
+
lines.append(f" access_count: {mem.access_count}")
|
|
23
|
+
return "\n".join(lines)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_scored_memory(sm: ScoredMemory) -> str:
|
|
27
|
+
"""Format a ScoredMemory with its score and components."""
|
|
28
|
+
lines = [
|
|
29
|
+
f"[{sm.memory.id}]",
|
|
30
|
+
f" score: {sm.score:.4f}",
|
|
31
|
+
f" type: {sm.memory.type}",
|
|
32
|
+
f" status: {sm.memory.status}",
|
|
33
|
+
f" text: {sm.memory.text}",
|
|
34
|
+
]
|
|
35
|
+
if sm.memory.gist:
|
|
36
|
+
lines.append(f" gist: {sm.memory.gist}")
|
|
37
|
+
if sm.memory.entities:
|
|
38
|
+
lines.append(f" entities: {', '.join(sm.memory.entities)}")
|
|
39
|
+
lines.append(f" strength: {sm.memory.strength:.2f}")
|
|
40
|
+
lines.append(f" confidence: {sm.memory.confidence:.2f}")
|
|
41
|
+
if sm.components:
|
|
42
|
+
parts = ", ".join(f"{k}={v:.3f}" for k, v in sm.components.items())
|
|
43
|
+
lines.append(f" components: {parts}")
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def format_recall_results(results: list[ScoredMemory]) -> str:
|
|
48
|
+
"""Format a list of recall results."""
|
|
49
|
+
if not results:
|
|
50
|
+
return "No memories found."
|
|
51
|
+
sections = [f"Found {len(results)} memories:\n"]
|
|
52
|
+
for i, sm in enumerate(results, 1):
|
|
53
|
+
sections.append(f"--- Result {i} ---")
|
|
54
|
+
sections.append(format_scored_memory(sm))
|
|
55
|
+
sections.append("")
|
|
56
|
+
return "\n".join(sections)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_stats(stats: dict) -> str:
|
|
60
|
+
"""Format memory store statistics."""
|
|
61
|
+
return "\n".join([
|
|
62
|
+
"Memory Store Statistics:",
|
|
63
|
+
f" Total memories: {stats['memory_count']}",
|
|
64
|
+
f" Active: {stats['active_count']}",
|
|
65
|
+
f" Superseded: {stats['superseded_count']}",
|
|
66
|
+
f" Contradicted: {stats['contradicted_count']}",
|
|
67
|
+
f" Total edges: {stats['edge_count']}",
|
|
68
|
+
f" Average strength: {stats['avg_strength']:.2f}",
|
|
69
|
+
])
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""OpenMem MCP server — persistent memory tools for Claude Code.
|
|
2
|
+
|
|
3
|
+
Exposes 7 tools via FastMCP (stdio transport):
|
|
4
|
+
memory_store, memory_recall, memory_link,
|
|
5
|
+
memory_reinforce, memory_supersede, memory_contradict,
|
|
6
|
+
memory_stats
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from mcp.server.fastmcp import FastMCP
|
|
16
|
+
|
|
17
|
+
# Ensure openmem is importable — handle both installed and dev setups
|
|
18
|
+
_repo_root = Path(__file__).resolve().parent.parent.parent
|
|
19
|
+
_src_dir = _repo_root / "src"
|
|
20
|
+
if _src_dir.is_dir() and str(_src_dir) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(_src_dir))
|
|
22
|
+
|
|
23
|
+
from openmem import MemoryEngine # noqa: E402
|
|
24
|
+
|
|
25
|
+
from formatting import format_memory, format_recall_results, format_stats # noqa: E402
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Initialisation
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
DEFAULT_DB = os.path.join(Path.home(), ".openmem", "memories.db")
|
|
32
|
+
db_path = os.environ.get("OPENMEM_DB", DEFAULT_DB)
|
|
33
|
+
|
|
34
|
+
# Ensure the DB directory exists
|
|
35
|
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
36
|
+
|
|
37
|
+
engine = MemoryEngine(db_path=db_path)
|
|
38
|
+
|
|
39
|
+
# Run decay pass on startup so stale memories lose strength naturally
|
|
40
|
+
engine.decay_all()
|
|
41
|
+
|
|
42
|
+
mcp = FastMCP(
|
|
43
|
+
"openmem",
|
|
44
|
+
description="Persistent cognitive memory for Claude Code",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Tools
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@mcp.tool()
|
|
53
|
+
def memory_store(
|
|
54
|
+
text: str,
|
|
55
|
+
type: str = "fact",
|
|
56
|
+
entities: list[str] | None = None,
|
|
57
|
+
confidence: float = 1.0,
|
|
58
|
+
gist: str | None = None,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Store a new memory.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
text: The content of the memory to store.
|
|
64
|
+
type: Memory type — one of: fact, decision, preference, incident, plan, constraint.
|
|
65
|
+
entities: Key entities mentioned (project names, people, technologies).
|
|
66
|
+
confidence: How certain this memory is, from 0.0 to 1.0.
|
|
67
|
+
gist: Optional one-line summary for quick retrieval.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Confirmation with the stored memory's ID and details.
|
|
71
|
+
"""
|
|
72
|
+
mem = engine.add(
|
|
73
|
+
text=text,
|
|
74
|
+
type=type,
|
|
75
|
+
entities=entities,
|
|
76
|
+
confidence=confidence,
|
|
77
|
+
gist=gist,
|
|
78
|
+
)
|
|
79
|
+
return f"Stored memory:\n{format_memory(mem)}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@mcp.tool()
|
|
83
|
+
def memory_recall(
|
|
84
|
+
query: str,
|
|
85
|
+
top_k: int = 5,
|
|
86
|
+
token_budget: int = 2000,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Recall memories relevant to a query.
|
|
89
|
+
|
|
90
|
+
Uses BM25 lexical search, spreading activation through the memory graph,
|
|
91
|
+
competition scoring, and conflict resolution to find the most relevant memories.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
query: The search query — what you want to remember.
|
|
95
|
+
top_k: Maximum number of memories to return.
|
|
96
|
+
token_budget: Approximate token budget for returned memories.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Formatted list of matching memories ranked by relevance.
|
|
100
|
+
"""
|
|
101
|
+
results = engine.recall(query=query, top_k=top_k, token_budget=token_budget)
|
|
102
|
+
return format_recall_results(results)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def memory_link(
|
|
107
|
+
source_id: str,
|
|
108
|
+
target_id: str,
|
|
109
|
+
rel_type: str = "mentions",
|
|
110
|
+
weight: float = 0.5,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Create a relationship between two memories.
|
|
113
|
+
|
|
114
|
+
Links enable spreading activation — related memories boost each other during recall.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
source_id: ID of the source memory.
|
|
118
|
+
target_id: ID of the target memory.
|
|
119
|
+
rel_type: Relationship type — one of: mentions, supports, contradicts, depends_on, same_as.
|
|
120
|
+
weight: Edge weight from 0.0 to 1.0, controls activation spread strength.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Confirmation with the edge details.
|
|
124
|
+
"""
|
|
125
|
+
edge = engine.link(
|
|
126
|
+
source_id=source_id,
|
|
127
|
+
target_id=target_id,
|
|
128
|
+
rel_type=rel_type,
|
|
129
|
+
weight=weight,
|
|
130
|
+
)
|
|
131
|
+
return f"Linked memories:\n edge_id: {edge.id}\n {source_id} --[{rel_type}, w={weight}]--> {target_id}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
def memory_reinforce(memory_id: str) -> str:
|
|
136
|
+
"""Reinforce a memory, boosting its strength.
|
|
137
|
+
|
|
138
|
+
Call this when a memory proves useful — it increases strength by 0.1 (up to 1.0)
|
|
139
|
+
and updates access stats. Reinforced memories rank higher in future recalls.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
memory_id: ID of the memory to reinforce.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Confirmation of the reinforcement.
|
|
146
|
+
"""
|
|
147
|
+
engine.reinforce(memory_id)
|
|
148
|
+
mem = engine.store.get_memory(memory_id)
|
|
149
|
+
if mem:
|
|
150
|
+
return f"Reinforced memory:\n{format_memory(mem)}"
|
|
151
|
+
return f"Memory {memory_id} not found."
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@mcp.tool()
|
|
155
|
+
def memory_supersede(old_id: str, new_id: str) -> str:
|
|
156
|
+
"""Mark an old memory as superseded by a newer one.
|
|
157
|
+
|
|
158
|
+
The old memory receives a 50% score penalty in future recalls.
|
|
159
|
+
A 'same_as' link is created from the new memory to the old one.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
old_id: ID of the outdated memory.
|
|
163
|
+
new_id: ID of the replacement memory.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Confirmation of the supersession.
|
|
167
|
+
"""
|
|
168
|
+
engine.supersede(old_id=old_id, new_id=new_id)
|
|
169
|
+
return f"Memory {old_id} superseded by {new_id}."
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@mcp.tool()
|
|
173
|
+
def memory_contradict(id_a: str, id_b: str) -> str:
|
|
174
|
+
"""Mark two memories as contradicting each other.
|
|
175
|
+
|
|
176
|
+
Creates a 'contradicts' edge. During recall, the weaker memory
|
|
177
|
+
(by strength x confidence x recency) is demoted to 30% of its score.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
id_a: ID of the first memory.
|
|
181
|
+
id_b: ID of the second memory.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Confirmation of the contradiction.
|
|
185
|
+
"""
|
|
186
|
+
engine.contradict(id_a=id_a, id_b=id_b)
|
|
187
|
+
return f"Memories {id_a} and {id_b} marked as contradicting."
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
def memory_stats() -> str:
|
|
192
|
+
"""Get summary statistics about the memory store.
|
|
193
|
+
|
|
194
|
+
Returns counts of total, active, superseded, and contradicted memories,
|
|
195
|
+
the number of edges, and average memory strength.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Formatted statistics summary.
|
|
199
|
+
"""
|
|
200
|
+
stats = engine.stats()
|
|
201
|
+
return format_stats(stats)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# Entry point
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
mcp.run(transport="stdio")
|