compound-memory 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.
Files changed (29) hide show
  1. compound_memory-0.1.0/LICENSE +21 -0
  2. compound_memory-0.1.0/PKG-INFO +150 -0
  3. compound_memory-0.1.0/README.md +121 -0
  4. compound_memory-0.1.0/pyproject.toml +50 -0
  5. compound_memory-0.1.0/setup.cfg +4 -0
  6. compound_memory-0.1.0/src/compound_memory/__init__.py +3 -0
  7. compound_memory-0.1.0/src/compound_memory/cli.py +190 -0
  8. compound_memory-0.1.0/src/compound_memory/embedding.py +96 -0
  9. compound_memory-0.1.0/src/compound_memory/index.py +203 -0
  10. compound_memory-0.1.0/src/compound_memory/model.py +44 -0
  11. compound_memory-0.1.0/src/compound_memory/review_queue.py +82 -0
  12. compound_memory-0.1.0/src/compound_memory/scoring.py +245 -0
  13. compound_memory-0.1.0/src/compound_memory/server.py +88 -0
  14. compound_memory-0.1.0/src/compound_memory/storage.py +693 -0
  15. compound_memory-0.1.0/src/compound_memory/vector_index.py +287 -0
  16. compound_memory-0.1.0/src/compound_memory.egg-info/PKG-INFO +150 -0
  17. compound_memory-0.1.0/src/compound_memory.egg-info/SOURCES.txt +27 -0
  18. compound_memory-0.1.0/src/compound_memory.egg-info/dependency_links.txt +1 -0
  19. compound_memory-0.1.0/src/compound_memory.egg-info/entry_points.txt +3 -0
  20. compound_memory-0.1.0/src/compound_memory.egg-info/requires.txt +15 -0
  21. compound_memory-0.1.0/src/compound_memory.egg-info/top_level.txt +1 -0
  22. compound_memory-0.1.0/tests/test_distill.py +282 -0
  23. compound_memory-0.1.0/tests/test_embedding.py +58 -0
  24. compound_memory-0.1.0/tests/test_index.py +245 -0
  25. compound_memory-0.1.0/tests/test_lifecycle.py +330 -0
  26. compound_memory-0.1.0/tests/test_mcp_tools.py +280 -0
  27. compound_memory-0.1.0/tests/test_model.py +23 -0
  28. compound_memory-0.1.0/tests/test_scoring.py +205 -0
  29. compound_memory-0.1.0/tests/test_vector_index.py +209 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chinwe
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.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: compound-memory
3
+ Version: 0.1.0
4
+ Summary: Local multi-agent shared memory with compounding (MCP server + CLI)
5
+ Author: chinwe
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/chinwe/compound-memory
8
+ Project-URL: Issues, https://github.com/chinwe/compound-memory/issues
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: mcp>=2
16
+ Requires-Dist: PyYAML>=6
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8; extra == "dev"
19
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
20
+ Requires-Dist: mypy>=1.10; extra == "dev"
21
+ Requires-Dist: types-PyYAML>=6; extra == "dev"
22
+ Requires-Dist: sqlite-vec>=0.1.9; extra == "dev"
23
+ Provides-Extra: vec
24
+ Requires-Dist: onnxruntime==1.19.2; extra == "vec"
25
+ Requires-Dist: tokenizers>=0.20; extra == "vec"
26
+ Requires-Dist: numpy>=1.26; extra == "vec"
27
+ Requires-Dist: sqlite-vec>=0.1.9; extra == "vec"
28
+ Dynamic: license-file
29
+
30
+ # compound-memory
31
+
32
+ 本地多 Agent 共享记忆系统——支持复利(越用越值钱)。Spec 见 `docs/specs/0001-compound-memory-spec.md`。
33
+
34
+ ## 架构
35
+
36
+ ```
37
+ Agent (MCP 客户端 / CLI)
38
+ └─ memory_write | memory_search | memory_get | memory_link | memory_feedback
39
+ └─ MemoryStore (~/.agents/memory)
40
+ ├─ namespaces/_shared/{episode,fact,insight,skill}/*.md 共享区
41
+ ├─ namespaces/agent-*/... 私有区
42
+ ├─ archive/... 衰减归档(可复活)
43
+ ├─ index/tokens.json 可重建的检索缓存
44
+ ├─ review-queue.md fact/insight 冲突队列
45
+ └─ .git/ 每次写入自动 commit
46
+ ```
47
+
48
+ ## 复利机制
49
+
50
+ | 利息来源 | 实现 |
51
+ |---|---|
52
+ | ① 使用强化 | `memory_feedback`: uses+1, conf+0.1 |
53
+ | ② 关联增值 | `memory_link` 双向关联;`memory_get` 带出一度邻居;`search` 命中自动内嵌精简邻居(上限 3、只召回活动记忆,`--no-neighbors` 可关) |
54
+ | ③ 蒸馏提纯 | `distill-plan`(CLI,确定性候选+双信号去重标注)→ Agent 判断 → `distill-apply` 原子落库(产物 links 溯源,源归档可复活) |
55
+ | ④ 跨 Agent 验证 | 与 source 不同的 agent 反馈时 conf 额外 +0.15 |
56
+
57
+ 评分公式(权重以 `src/compound_memory/scoring.py` 的 `W_*` 常量为准):`0.70·相似度 + 0.15·置信度 + 0.10·新近度(0.5+0.5·e^(−Δt/τ)) + 0.05·类型权重`;双路(向量路启用)时改为 RRF 融合主序 + ε=0.04 先验 tie-break(见 spec「索引即缓存」)。
58
+
59
+ ## MCP 接入
60
+
61
+ 各宿主(WorkBuddy / ZCode / Claude Code / DeepSeek Harness)的完整接入配置与统一使用规范见 `docs/agent-integration.md`。5 个 tool 的 description 自带闭环铁律(命中采纳后必须回写 `memory_feedback`、只写稳定事实、复用既有 key),宿主不注入使用规范也能保持复利闭环——注入规范(agent-integration §6)仍推荐,用于收紧写入质量。
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "compound-memory": {
67
+ "type": "stdio",
68
+ "command": "~/.local/bin/uv",
69
+ "args": ["run", "--directory", "<本目录>", "compound-memory-server"],
70
+ "env": { "COMPOUND_MEMORY_ROOT": "~/.agents/memory" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ 各宿主配置若不展开 `~` 占位写法,替换为本机绝对路径即可。向量召回路为可选(`uv sync --extra vec`):未装 extra 或 HF 缓存缺模型时自动降级纯词面。embedding 模型与维度可经 `COMPOUND_MEMORY_EMBEDDING_MODEL`(默认 `Xenova/bge-small-zh-v1.5`)与 `COMPOUND_MEMORY_EMBEDDING_DIM`(默认 512)覆盖——换模型属运维动作,改后需显式 `rebuild-index`。
77
+
78
+ ## CLI
79
+
80
+ ```bash
81
+ uv sync --extra dev # 首次克隆后初始化 .venv(之后 uv run 自动使用)
82
+
83
+ uv run compound-memory init # 初始化空库
84
+ uv run compound-memory write "Vercel Serverless 10s 超时" episode agent-workbuddy
85
+ uv run compound-memory search "Vercel 超时" # 命中内嵌一度邻居(上限3,--no-neighbors 关闭)
86
+ uv run compound-memory feedback <id> agent-claude
87
+ uv run compound-memory decay # cron 定时跑
88
+ uv run compound-memory revive <id> # 复活归档记忆(CLI 唯一入口)
89
+ uv run compound-memory distill-plan # 蒸馏候选清单:merge_with(同 key 强信号)+ possible_dup_of(BM25 弱信号)+ promotion_candidate(高活性 episode)
90
+ uv run compound-memory distill-apply "合并后的经验" insight agent-workbuddy --sources <id1>,<id2> # 原子落库:产物(links 溯源, origin=distillation) + 源归档,一次 commit
91
+ uv run compound-memory stats # 健康度:uses/confidence 固定桶 + 活性 + 蒸馏产出量
92
+ uv run compound-memory rebuild-index # 索引可随时重建
93
+ uv run compound-memory review-queue # 冲突队列(CLI 唯一入口)
94
+ uv run compound-memory git-log # 审计轨迹
95
+ ```
96
+
97
+ ## 定时蒸馏准备(launchd / cron / systemd)
98
+
99
+ ADR 0001:确定性准备定时跑,判断(摘要/合并)由 Agent 会话内按需完成。每天 09:00 把候选清单写到 `<root>/distill/last-plan.json`。调度器三选一:**launchd**(macOS 系统标准,睡眠错过的计划唤醒后补跑)、**systemd user timer**(`Persistent=true` 同样补跑)、**cron**(最通用但不补跑错过的计划)。三者都调用同一个平台无关的 `scripts/distill-prepare.sh`。
100
+
101
+ **launchd(macOS)**:
102
+
103
+ ```bash
104
+ REPO=$(pwd); UV="$HOME/.local/bin/uv" # 项目环境由 uv 管理,脚本内经 UV_BIN 覆盖 launchd PATH
105
+ sed -e "s|__REPO__|$REPO|g" -e "s|__UV__|$UV|g" -e "s|__ROOT__|$HOME/.agents/memory|g" \
106
+ scripts/com.compound-memory.distill-prepare.plist.tmpl \
107
+ > ~/Library/LaunchAgents/com.compound-memory.distill-prepare.plist
108
+ launchctl load ~/Library/LaunchAgents/com.compound-memory.distill-prepare.plist
109
+ launchctl list | grep compound-memory # 验证已加载;日志在 <root>/distill/prepare.log
110
+ ```
111
+
112
+ **systemd user(Linux)**:
113
+
114
+ ```bash
115
+ REPO=$(pwd); UV="$HOME/.local/bin/uv"
116
+ mkdir -p ~/.config/systemd/user
117
+ for f in service timer; do
118
+ sed -e "s|__REPO__|$REPO|g" -e "s|__UV__|$UV|g" -e "s|__ROOT__|$HOME/.agents/memory|g" \
119
+ scripts/compound-memory-distill-prepare.$f.example \
120
+ > ~/.config/systemd/user/compound-memory-distill-prepare.$f
121
+ done
122
+ systemctl --user daemon-reload
123
+ systemctl --user enable --now compound-memory-distill-prepare.timer
124
+ systemctl --user list-timers | grep compound-memory # 验证已加载
125
+ ```
126
+
127
+ **cron(其他环境)**:`crontab -e` 加入(sed 填充占位符后):
128
+
129
+ ```
130
+ 0 9 * * * UV_BIN=$HOME/.local/bin/uv COMPOUND_MEMORY_ROOT=$HOME/.agents/memory /bin/sh <仓库>/scripts/distill-prepare.sh >> $HOME/.agents/memory/distill/prepare.log 2>&1
131
+ ```
132
+
133
+ 失败要响亮:脚本 `set -eu`,任何一步失败以非 0 退出(`launchctl list` / `systemctl --user list-units` / cron 邮件可见,日志落 distill/prepare.log)。`distill/` 是运行时产物目录(自动加入库 .gitignore),不产生 commit 噪声——只有 Agent 判断后跑 `distill-apply` 才落一次原子 commit。
134
+
135
+ ## 开发
136
+
137
+ ```bash
138
+ uv run pytest tests/ -q # 85 tests(MCP tool 边界 + 蒸馏 + 生命周期/索引/CLI)
139
+ uv run mypy src/compound_memory/
140
+ ```
141
+
142
+ 测试缝:MCP tool 边界(`mcp.Client(server)` 内存直连,无子进程)+ 核心模块单测(scoring / index / store 运维面)。CI 在 Python 3.11/3.12/3.13 矩阵上跑测试、类型检查与纯 wheel 安装冒烟。
143
+
144
+ ## 发布
145
+
146
+ PyPI 版本不可重传,tag 必须与 `pyproject.toml` 的 `version` 一致(release workflow 有校验,不一致响亮失败)。发布走 GitHub Actions + PyPI Trusted Publisher(OIDC,免 token):
147
+
148
+ 1. **一次性配置**(PyPI → 项目 → Publishing):owner `chinwe`、repo `compound-memory`、workflow `release.yml`、environment `pypi`。首次发布时项目尚不存在,在 pypi.org 用"pending publisher"预注册即可。
149
+ 2. **发布**:`git tag v0.1.0 && git push origin v0.1.0` → `release.yml` 自动 build + `uv publish`。
150
+ 3. 发布后 `uvx compound-memory-server` 即为通用安装形态(MCP 配置里的 `command` 也可换成 `uvx`,不再依赖仓库克隆路径)。
@@ -0,0 +1,121 @@
1
+ # compound-memory
2
+
3
+ 本地多 Agent 共享记忆系统——支持复利(越用越值钱)。Spec 见 `docs/specs/0001-compound-memory-spec.md`。
4
+
5
+ ## 架构
6
+
7
+ ```
8
+ Agent (MCP 客户端 / CLI)
9
+ └─ memory_write | memory_search | memory_get | memory_link | memory_feedback
10
+ └─ MemoryStore (~/.agents/memory)
11
+ ├─ namespaces/_shared/{episode,fact,insight,skill}/*.md 共享区
12
+ ├─ namespaces/agent-*/... 私有区
13
+ ├─ archive/... 衰减归档(可复活)
14
+ ├─ index/tokens.json 可重建的检索缓存
15
+ ├─ review-queue.md fact/insight 冲突队列
16
+ └─ .git/ 每次写入自动 commit
17
+ ```
18
+
19
+ ## 复利机制
20
+
21
+ | 利息来源 | 实现 |
22
+ |---|---|
23
+ | ① 使用强化 | `memory_feedback`: uses+1, conf+0.1 |
24
+ | ② 关联增值 | `memory_link` 双向关联;`memory_get` 带出一度邻居;`search` 命中自动内嵌精简邻居(上限 3、只召回活动记忆,`--no-neighbors` 可关) |
25
+ | ③ 蒸馏提纯 | `distill-plan`(CLI,确定性候选+双信号去重标注)→ Agent 判断 → `distill-apply` 原子落库(产物 links 溯源,源归档可复活) |
26
+ | ④ 跨 Agent 验证 | 与 source 不同的 agent 反馈时 conf 额外 +0.15 |
27
+
28
+ 评分公式(权重以 `src/compound_memory/scoring.py` 的 `W_*` 常量为准):`0.70·相似度 + 0.15·置信度 + 0.10·新近度(0.5+0.5·e^(−Δt/τ)) + 0.05·类型权重`;双路(向量路启用)时改为 RRF 融合主序 + ε=0.04 先验 tie-break(见 spec「索引即缓存」)。
29
+
30
+ ## MCP 接入
31
+
32
+ 各宿主(WorkBuddy / ZCode / Claude Code / DeepSeek Harness)的完整接入配置与统一使用规范见 `docs/agent-integration.md`。5 个 tool 的 description 自带闭环铁律(命中采纳后必须回写 `memory_feedback`、只写稳定事实、复用既有 key),宿主不注入使用规范也能保持复利闭环——注入规范(agent-integration §6)仍推荐,用于收紧写入质量。
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "compound-memory": {
38
+ "type": "stdio",
39
+ "command": "~/.local/bin/uv",
40
+ "args": ["run", "--directory", "<本目录>", "compound-memory-server"],
41
+ "env": { "COMPOUND_MEMORY_ROOT": "~/.agents/memory" }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ 各宿主配置若不展开 `~` 占位写法,替换为本机绝对路径即可。向量召回路为可选(`uv sync --extra vec`):未装 extra 或 HF 缓存缺模型时自动降级纯词面。embedding 模型与维度可经 `COMPOUND_MEMORY_EMBEDDING_MODEL`(默认 `Xenova/bge-small-zh-v1.5`)与 `COMPOUND_MEMORY_EMBEDDING_DIM`(默认 512)覆盖——换模型属运维动作,改后需显式 `rebuild-index`。
48
+
49
+ ## CLI
50
+
51
+ ```bash
52
+ uv sync --extra dev # 首次克隆后初始化 .venv(之后 uv run 自动使用)
53
+
54
+ uv run compound-memory init # 初始化空库
55
+ uv run compound-memory write "Vercel Serverless 10s 超时" episode agent-workbuddy
56
+ uv run compound-memory search "Vercel 超时" # 命中内嵌一度邻居(上限3,--no-neighbors 关闭)
57
+ uv run compound-memory feedback <id> agent-claude
58
+ uv run compound-memory decay # cron 定时跑
59
+ uv run compound-memory revive <id> # 复活归档记忆(CLI 唯一入口)
60
+ uv run compound-memory distill-plan # 蒸馏候选清单:merge_with(同 key 强信号)+ possible_dup_of(BM25 弱信号)+ promotion_candidate(高活性 episode)
61
+ uv run compound-memory distill-apply "合并后的经验" insight agent-workbuddy --sources <id1>,<id2> # 原子落库:产物(links 溯源, origin=distillation) + 源归档,一次 commit
62
+ uv run compound-memory stats # 健康度:uses/confidence 固定桶 + 活性 + 蒸馏产出量
63
+ uv run compound-memory rebuild-index # 索引可随时重建
64
+ uv run compound-memory review-queue # 冲突队列(CLI 唯一入口)
65
+ uv run compound-memory git-log # 审计轨迹
66
+ ```
67
+
68
+ ## 定时蒸馏准备(launchd / cron / systemd)
69
+
70
+ ADR 0001:确定性准备定时跑,判断(摘要/合并)由 Agent 会话内按需完成。每天 09:00 把候选清单写到 `<root>/distill/last-plan.json`。调度器三选一:**launchd**(macOS 系统标准,睡眠错过的计划唤醒后补跑)、**systemd user timer**(`Persistent=true` 同样补跑)、**cron**(最通用但不补跑错过的计划)。三者都调用同一个平台无关的 `scripts/distill-prepare.sh`。
71
+
72
+ **launchd(macOS)**:
73
+
74
+ ```bash
75
+ REPO=$(pwd); UV="$HOME/.local/bin/uv" # 项目环境由 uv 管理,脚本内经 UV_BIN 覆盖 launchd PATH
76
+ sed -e "s|__REPO__|$REPO|g" -e "s|__UV__|$UV|g" -e "s|__ROOT__|$HOME/.agents/memory|g" \
77
+ scripts/com.compound-memory.distill-prepare.plist.tmpl \
78
+ > ~/Library/LaunchAgents/com.compound-memory.distill-prepare.plist
79
+ launchctl load ~/Library/LaunchAgents/com.compound-memory.distill-prepare.plist
80
+ launchctl list | grep compound-memory # 验证已加载;日志在 <root>/distill/prepare.log
81
+ ```
82
+
83
+ **systemd user(Linux)**:
84
+
85
+ ```bash
86
+ REPO=$(pwd); UV="$HOME/.local/bin/uv"
87
+ mkdir -p ~/.config/systemd/user
88
+ for f in service timer; do
89
+ sed -e "s|__REPO__|$REPO|g" -e "s|__UV__|$UV|g" -e "s|__ROOT__|$HOME/.agents/memory|g" \
90
+ scripts/compound-memory-distill-prepare.$f.example \
91
+ > ~/.config/systemd/user/compound-memory-distill-prepare.$f
92
+ done
93
+ systemctl --user daemon-reload
94
+ systemctl --user enable --now compound-memory-distill-prepare.timer
95
+ systemctl --user list-timers | grep compound-memory # 验证已加载
96
+ ```
97
+
98
+ **cron(其他环境)**:`crontab -e` 加入(sed 填充占位符后):
99
+
100
+ ```
101
+ 0 9 * * * UV_BIN=$HOME/.local/bin/uv COMPOUND_MEMORY_ROOT=$HOME/.agents/memory /bin/sh <仓库>/scripts/distill-prepare.sh >> $HOME/.agents/memory/distill/prepare.log 2>&1
102
+ ```
103
+
104
+ 失败要响亮:脚本 `set -eu`,任何一步失败以非 0 退出(`launchctl list` / `systemctl --user list-units` / cron 邮件可见,日志落 distill/prepare.log)。`distill/` 是运行时产物目录(自动加入库 .gitignore),不产生 commit 噪声——只有 Agent 判断后跑 `distill-apply` 才落一次原子 commit。
105
+
106
+ ## 开发
107
+
108
+ ```bash
109
+ uv run pytest tests/ -q # 85 tests(MCP tool 边界 + 蒸馏 + 生命周期/索引/CLI)
110
+ uv run mypy src/compound_memory/
111
+ ```
112
+
113
+ 测试缝:MCP tool 边界(`mcp.Client(server)` 内存直连,无子进程)+ 核心模块单测(scoring / index / store 运维面)。CI 在 Python 3.11/3.12/3.13 矩阵上跑测试、类型检查与纯 wheel 安装冒烟。
114
+
115
+ ## 发布
116
+
117
+ PyPI 版本不可重传,tag 必须与 `pyproject.toml` 的 `version` 一致(release workflow 有校验,不一致响亮失败)。发布走 GitHub Actions + PyPI Trusted Publisher(OIDC,免 token):
118
+
119
+ 1. **一次性配置**(PyPI → 项目 → Publishing):owner `chinwe`、repo `compound-memory`、workflow `release.yml`、environment `pypi`。首次发布时项目尚不存在,在 pypi.org 用"pending publisher"预注册即可。
120
+ 2. **发布**:`git tag v0.1.0 && git push origin v0.1.0` → `release.yml` 自动 build + `uv publish`。
121
+ 3. 发布后 `uvx compound-memory-server` 即为通用安装形态(MCP 配置里的 `command` 也可换成 `uvx`,不再依赖仓库克隆路径)。
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "compound-memory"
7
+ version = "0.1.0"
8
+ description = "Local multi-agent shared memory with compounding (MCP server + CLI)"
9
+ readme = "README.md"
10
+ # PEP 639 SPDX 表达式;与 License:: classifier 互斥,故后者已移除
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "chinwe"}]
14
+ requires-python = ">=3.11"
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ ]
20
+ dependencies = ["mcp>=2", "PyYAML>=6"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/chinwe/compound-memory"
24
+ Issues = "https://github.com/chinwe/compound-memory/issues"
25
+
26
+ [project.scripts]
27
+ compound-memory = "compound_memory.cli:main"
28
+ compound-memory-server = "compound_memory.server:main"
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.10", "types-PyYAML>=6", "sqlite-vec>=0.1.9"]
32
+ # 向量召回路(可选):未安装时 search 自动降级纯词面(检索降级不报错)。
33
+ # onnxruntime 钉 1.19.2:macOS x64 可装的最新版(1.20+ 的 wheel 标签要求 macOS 13+)
34
+ vec = ["onnxruntime==1.19.2", "tokenizers>=0.20", "numpy>=1.26", "sqlite-vec>=0.1.9"]
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.pytest.ini_options]
40
+ asyncio_mode = "auto"
41
+ testpaths = ["tests"]
42
+
43
+ [tool.mypy]
44
+ python_version = "3.13"
45
+ strict = false
46
+ warn_unused_ignores = true
47
+
48
+ [[tool.mypy.overrides]]
49
+ module = ["onnxruntime.*", "onnxruntime", "tokenizers.*", "tokenizers", "sqlite_vec.*", "sqlite_vec", "numpy.*", "numpy"]
50
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """compound-memory: local multi-agent shared memory with compounding."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,190 @@
1
+ """CLI:不经 MCP 直接访问同一 store(脚本、定时蒸馏、衰减扫描等运维面)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import datetime as dt
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from . import __version__
13
+ from .embedding import auto_encoder
14
+ from .storage import DISTILL_DUP_SIM_THRESHOLD, MEMORY_TYPES, MemoryStore, PROMOTION_USES_THRESHOLD, default_root
15
+
16
+
17
+ def _emit(payload: Any) -> None:
18
+ print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
19
+
20
+
21
+ def _open_store(args: argparse.Namespace) -> MemoryStore:
22
+ # CLI 与 server 同权:vec extra + 模型就绪即启用向量路,否则自动降级纯词面
23
+ return MemoryStore(Path(args.root), embedder=auto_encoder())
24
+
25
+
26
+ def cmd_init(args: argparse.Namespace) -> None:
27
+ store = _open_store(args)
28
+ _emit({"ok": True, "root": str(store.root)})
29
+
30
+
31
+ def cmd_write(args: argparse.Namespace) -> None:
32
+ _emit(_open_store(args).write(content=args.content, type=args.type, source=args.source, ns=args.ns, key=args.key))
33
+
34
+
35
+ def cmd_search(args: argparse.Namespace) -> None:
36
+ _emit(
37
+ _open_store(args).search(
38
+ query=args.query, ns=args.ns, top_k=args.top_k, include_neighbors=args.include_neighbors
39
+ )
40
+ )
41
+
42
+
43
+ def cmd_get(args: argparse.Namespace) -> None:
44
+ _emit(_open_store(args).get(args.id))
45
+
46
+
47
+ def cmd_link(args: argparse.Namespace) -> None:
48
+ _emit(_open_store(args).link(args.a, args.b))
49
+
50
+
51
+ def cmd_feedback(args: argparse.Namespace) -> None:
52
+ _emit(_open_store(args).feedback(args.id, args.agent))
53
+
54
+
55
+ def cmd_decay(args: argparse.Namespace) -> None:
56
+ # --now 经固定 clock 的 store 注入——时间接缝只有 clock 一条(store 不另设 now= 参数)
57
+ if args.now:
58
+ store = MemoryStore(Path(args.root), clock=lambda: dt.date.fromisoformat(args.now))
59
+ else:
60
+ store = _open_store(args)
61
+ _emit({"archived": store.decay_sweep()})
62
+
63
+
64
+ def cmd_revive(args: argparse.Namespace) -> None:
65
+ _emit(_open_store(args).revive(args.id))
66
+
67
+
68
+ def cmd_stats(args: argparse.Namespace) -> None:
69
+ _emit(_open_store(args).stats())
70
+
71
+
72
+ def cmd_rebuild_index(args: argparse.Namespace) -> None:
73
+ _emit(_open_store(args).rebuild_index())
74
+
75
+
76
+ def cmd_review_queue(args: argparse.Namespace) -> None:
77
+ _emit(_open_store(args).review_queue())
78
+
79
+
80
+ def cmd_review_resolve(args: argparse.Namespace) -> None:
81
+ _emit(_open_store(args).review_resolve(ids=args.ids, all=args.all))
82
+
83
+
84
+ def cmd_distill_plan(args: argparse.Namespace) -> None:
85
+ _emit(
86
+ _open_store(args).distill_plan(
87
+ window_days=args.window,
88
+ min_uses=args.min_uses,
89
+ min_confidence=args.min_confidence,
90
+ ns=args.ns,
91
+ )
92
+ )
93
+
94
+
95
+ def cmd_distill_apply(args: argparse.Namespace) -> None:
96
+ _emit(
97
+ _open_store(args).distill_apply(
98
+ content=args.content,
99
+ type=args.type,
100
+ source=args.source,
101
+ source_ids=[s.strip() for s in args.sources.split(",") if s.strip()],
102
+ ns=args.ns,
103
+ key=args.key,
104
+ confidence=args.confidence,
105
+ )
106
+ )
107
+
108
+
109
+ def cmd_git_log(args: argparse.Namespace) -> None:
110
+ _emit(_open_store(args).git_log(limit=args.limit))
111
+
112
+
113
+ def build_parser() -> argparse.ArgumentParser:
114
+ parser = argparse.ArgumentParser(prog="compound-memory", description="local multi-agent shared memory")
115
+ parser.add_argument("--version", action="version", version=__version__)
116
+ parser.add_argument("--root", default=None, help="store root (default: $COMPOUND_MEMORY_ROOT or ~/.agents/memory)")
117
+ sub = parser.add_subparsers(dest="command", required=True)
118
+
119
+ p = sub.add_parser("init"); p.set_defaults(func=cmd_init)
120
+
121
+ p = sub.add_parser("write")
122
+ p.add_argument("content"); p.add_argument("type", choices=MEMORY_TYPES)
123
+ p.add_argument("source"); p.add_argument("--ns", default="_shared"); p.add_argument("--key", default=None)
124
+ p.set_defaults(func=cmd_write)
125
+
126
+ p = sub.add_parser("search")
127
+ p.add_argument("query"); p.add_argument("--ns", default="_shared"); p.add_argument("--top-k", type=int, default=5)
128
+ p.add_argument("--no-neighbors", dest="include_neighbors", action="store_false",
129
+ help="omit embedded one-hop neighbors from hits")
130
+ p.set_defaults(func=cmd_search)
131
+
132
+ p = sub.add_parser("get"); p.add_argument("id"); p.set_defaults(func=cmd_get)
133
+ p = sub.add_parser("link"); p.add_argument("a"); p.add_argument("b"); p.set_defaults(func=cmd_link)
134
+ p = sub.add_parser("feedback"); p.add_argument("id"); p.add_argument("agent"); p.set_defaults(func=cmd_feedback)
135
+
136
+ p = sub.add_parser("decay"); p.add_argument("--now", default=None, help="ISO date override (testing)")
137
+ p.set_defaults(func=cmd_decay)
138
+
139
+ p = sub.add_parser("revive"); p.add_argument("id"); p.set_defaults(func=cmd_revive)
140
+ p = sub.add_parser(
141
+ "distill-plan",
142
+ help="scan distillation candidates and print a signal-annotated list",
143
+ description="Scan distillation candidates (deterministic half of distillation). Signals per candidate: "
144
+ f"merge_with (same ns/type/key, strong), possible_dup_of (BM25 normalized_similarity >= {DISTILL_DUP_SIM_THRESHOLD}, weak), "
145
+ f"promotion_candidate (episode uses >= {PROMOTION_USES_THRESHOLD}). Judgment (merge/summarize) stays with the calling agent.",
146
+ )
147
+ p.add_argument("--window", type=int, default=30, help="recency window in days (last_used first, created fallback)")
148
+ p.add_argument("--min-uses", type=int, default=1, help="activity gate: uses >= this")
149
+ p.add_argument("--min-confidence", type=float, default=0.5, help="activity gate: confidence >= this")
150
+ p.add_argument("--ns", default="_shared")
151
+ p.set_defaults(func=cmd_distill_plan)
152
+ p = sub.add_parser("distill-apply")
153
+ p.add_argument("content"); p.add_argument("type", choices=MEMORY_TYPES); p.add_argument("source")
154
+ p.add_argument("--sources", required=True, help="comma-separated source memory ids")
155
+ p.add_argument("--ns", default="_shared"); p.add_argument("--key", default=None)
156
+ p.add_argument("--confidence", type=float, default=None)
157
+ p.set_defaults(func=cmd_distill_apply)
158
+ p = sub.add_parser("stats"); p.set_defaults(func=cmd_stats)
159
+ p = sub.add_parser("rebuild-index"); p.set_defaults(func=cmd_rebuild_index)
160
+ p = sub.add_parser("review-queue"); p.set_defaults(func=cmd_review_queue)
161
+ p = sub.add_parser(
162
+ "review-resolve",
163
+ help="mark review-queue conflicts as resolved",
164
+ description="Clear review-queue entries after the caller has judged the conflict "
165
+ "(old/new trade-off stays with the calling agent or human). Pass memory ids to clear "
166
+ "matching rows (either the old or new id of a row counts), or --all to clear the queue. "
167
+ "Unknown ids are rejected atomically; the cleanup is auto-committed.",
168
+ )
169
+ p.add_argument("ids", nargs="*", metavar="ID")
170
+ p.add_argument("--all", action="store_true", help="clear the whole queue")
171
+ p.set_defaults(func=cmd_review_resolve)
172
+ p = sub.add_parser("git-log"); p.add_argument("--limit", type=int, default=5); p.set_defaults(func=cmd_git_log)
173
+ return parser
174
+
175
+
176
+ def main(argv: list[str] | None = None) -> int:
177
+ args = build_parser().parse_args(argv)
178
+ if args.root is None:
179
+ args.root = default_root()
180
+ try:
181
+ args.func(args)
182
+ return 0
183
+ except (ValueError, PermissionError) as exc:
184
+ # store 接口的调用方错误统一在这里翻译成 JSON(MCP 侧由框架转 is_error)
185
+ print(json.dumps({"error": str(exc)}, ensure_ascii=False), file=sys.stderr)
186
+ return 2
187
+
188
+
189
+ if __name__ == "__main__":
190
+ sys.exit(main())
@@ -0,0 +1,96 @@
1
+ """BGE-small-zh 本地 embedding 编码器(向量召回路的模型缝)。
2
+
3
+ - 模型:默认 Xenova/bge-small-zh-v1.5 ONNX int8(24MB,经 hf-mirror 下载到 HF 缓存,
4
+ 本地推理全程无外发,spec story 17)。缓存缺失时构造即失败——不自动联网下载。
5
+ repo id 与输出维度可经环境变量覆盖(换模型属运维动作,改后需显式 rebuild-index):
6
+ - COMPOUND_MEMORY_EMBEDDING_MODEL:HF repo id(默认 Xenova/bge-small-zh-v1.5);
7
+ - COMPOUND_MEMORY_EMBEDDING_DIM:模型输出维度(默认 512,须与模型一致)。
8
+ - 依赖:属可选 extra `vec`(onnxruntime/tokenizers/numpy)。本模块 import 任何
9
+ 失败都置 VEC_AVAILABLE=False,由调用方降级为纯词面检索(检索降级不报错)。
10
+ - 编码语义(与 vec-spike 验证一致):[CLS] 表示 + L2 归一化,truncation 512。
11
+ 单条长文档实测 ~400ms,查询短文本 ~15ms。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import glob
17
+ import os
18
+ from pathlib import Path
19
+ from typing import Callable
20
+
21
+ # 模型与维度的单一定义点:vector_index 建表维度也从这里 import,勿另设常量
22
+ MODEL_REPO_ID = os.environ.get("COMPOUND_MEMORY_EMBEDDING_MODEL", "Xenova/bge-small-zh-v1.5")
23
+ EMBED_DIM = int(os.environ.get("COMPOUND_MEMORY_EMBEDDING_DIM", "512"))
24
+
25
+
26
+ def _cache_glob(repo_id: str) -> str:
27
+ """HF 缓存目录 glob:repo id 的 "/" 替换为 "--"(如 a/b → models--a--b)。"""
28
+ return f".cache/huggingface/hub/models--{repo_id.replace('/', '--')}/snapshots/*"
29
+
30
+
31
+ _MODEL_GLOB = _cache_glob(MODEL_REPO_ID)
32
+
33
+ try:
34
+ import numpy as np
35
+ import onnxruntime as ort
36
+ from tokenizers import Tokenizer
37
+
38
+ VEC_AVAILABLE = True
39
+ except ImportError: # pragma: no cover - 取决于安装环境是否带 vec extra
40
+ VEC_AVAILABLE = False
41
+
42
+
43
+ class ModelMissingError(RuntimeError):
44
+ """HF 缓存中找不到 BGE ONNX 模型(本地优先:不做自动下载,交调用方降级)。"""
45
+
46
+
47
+ def auto_encoder() -> "Callable[[list[str]], list[list[float]]] | None":
48
+ """入口默认缝:依赖与模型都就绪才返回编码器(encode 绑定方法),否则 None(降级纯词面)。"""
49
+ if not VEC_AVAILABLE:
50
+ return None
51
+ try:
52
+ return BgeEncoder().encode
53
+ except (ModelMissingError, OSError):
54
+ return None
55
+
56
+
57
+ class BgeEncoder:
58
+ """惰性加载的 ONNX 编码器;构造只解析模型路径,首次 encode 才建 session。"""
59
+
60
+ def __init__(self, home: Path | None = None) -> None:
61
+ if not VEC_AVAILABLE:
62
+ raise ModelMissingError("vec dependencies not installed (pip install 'compound-memory[vec]')")
63
+ base = home or Path.home()
64
+ snaps = sorted(glob.glob(str(base / _MODEL_GLOB)))
65
+ if not snaps:
66
+ raise ModelMissingError(
67
+ f"{MODEL_REPO_ID} ONNX model not found in HF cache; "
68
+ "download via hf-mirror.com (see docs/specs/0001-compound-memory-spec.md)"
69
+ )
70
+ snap = Path(snaps[-1])
71
+ self._onnx_path = snap / "onnx" / "model_quantized.onnx"
72
+ self._tokenizer_path = snap / "tokenizer.json"
73
+ if not self._onnx_path.exists() or not self._tokenizer_path.exists():
74
+ raise ModelMissingError(f"incomplete model snapshot: {snap}")
75
+ self._session: "ort.InferenceSession | None" = None
76
+ self._tokenizer: "Tokenizer | None" = None
77
+
78
+ def encode(self, texts: list[str]) -> list[list[float]]:
79
+ """批量编码;L2 归一化后的 [CLS] 表示(余弦可直接用作相似度)。"""
80
+ assert VEC_AVAILABLE # 构造已保证;reassure 类型检查
81
+ if self._session is None or self._tokenizer is None:
82
+ self._tokenizer = Tokenizer.from_file(str(self._tokenizer_path))
83
+ self._tokenizer.enable_truncation(max_length=512)
84
+ self._tokenizer.enable_padding()
85
+ self._session = ort.InferenceSession(str(self._onnx_path), providers=["CPUExecutionProvider"])
86
+ encs = self._tokenizer.encode_batch(texts)
87
+ feed = {
88
+ "input_ids": np.array([e.ids for e in encs], dtype=np.int64),
89
+ "attention_mask": np.array([e.attention_mask for e in encs], dtype=np.int64),
90
+ "token_type_ids": np.array([e.type_ids for e in encs], dtype=np.int64),
91
+ }
92
+ names = {i.name for i in self._session.get_inputs()}
93
+ out = self._session.run(None, {k: v for k, v in feed.items() if k in names})[0]
94
+ cls = out[:, 0, :]
95
+ normed = cls / np.linalg.norm(cls, axis=1, keepdims=True)
96
+ return normed.tolist()