@aipack-ai/memory 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aipack
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.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # aipack-memory
2
+
3
+ > aipack 持久化记忆插件:**capture → compress → index → recall/inject → consolidate**
4
+ >
5
+ > 参考 [rohitg00/agentmemory](https://github.com/rohitg00/agentmemory),为 [aipack](../aipack) 提供「跨会话长期记忆」能力。
6
+
7
+ ## 特性
8
+
9
+ - **自动捕获**:每轮对话结束自动提取要点存为可检索记忆(零-LLM 要点压缩,可选 LLM 摘要)
10
+ - **自动注入**:每轮对话开始自动检索相关记忆,注入到最新 user 消息(sentinel 机制,防跨轮累积)
11
+ - **BM25 检索**:零依赖关键词检索,支持 CJK(中日韩,bigram)与 Latin 分词
12
+ - **混合检索**:提供 `Embedder` 接口后自动升级为 BM25 + 向量**双路独立召回**融合(向量召回不被 BM25 top-K 封顶)
13
+ - **记忆合并**:增量去重 / 合并相似记忆(O(N²) → 增量窗口),修剪过期与低置信度条目
14
+ - **并发安全**:同 id 写操作经 keyed mutex 串行化,capture 按 sessionKey 配对
15
+ - **可观测性**:`MemoryEvent` 事件上报(失败/整理/加载)与 `stats()` 统计快照
16
+ - **Agent 工具**:4 个可调用工具(save / search / list / delete),带输入校验与可选 TTL
17
+ - **零配置开箱即用**:默认零依赖、零 API Key
18
+
19
+ ## 安装
20
+
21
+ ```bash
22
+ pnpm add aipack-memory
23
+ # 或
24
+ npm install aipack-memory
25
+ ```
26
+
27
+ `aipack` 为 peer 依赖,需同时安装。
28
+
29
+ ## 快速接入
30
+
31
+ ### aipack.config.js
32
+
33
+ ```js
34
+ import { createMemoryPlugin } from 'aipack-memory';
35
+
36
+ const mem = createMemoryPlugin({
37
+ baseDir: '~/.aipack/memory', // 记忆存储目录
38
+ maxMemories: 5, // 每轮注入 top-5
39
+ });
40
+
41
+ const r = mem.install();
42
+
43
+ export default {
44
+ provider: 'deepseek',
45
+ model: 'deepseek-v4-flash',
46
+ systemPrompt: '你是一个有用的助手',
47
+ sessions: { enabled: true, baseDir: './sessions', maxAge: 30 },
48
+ extensions: r.extensions,
49
+ transformers: r.transformers,
50
+ tools: r.tools,
51
+ };
52
+ ```
53
+
54
+ ### 编程式 API
55
+
56
+ ```typescript
57
+ import { createMemoryPlugin, InMemoryStore } from 'aipack-memory';
58
+
59
+ const store = new InMemoryStore();
60
+ const mem = createMemoryPlugin({ store });
61
+
62
+ // 直接操作 store
63
+ await mem.store.save({
64
+ content: '用户偏好深色主题',
65
+ concepts: ['ui', 'dark-mode'],
66
+ confidence: 0.8,
67
+ source: 'tool',
68
+ });
69
+
70
+ const results = await mem.store.search('主题偏好', 5);
71
+ console.log(results[0]?.entry.content); // '用户偏好深色主题'
72
+
73
+ // 手动触发合并
74
+ await mem.store.consolidate({ similarityThreshold: 0.85 });
75
+ ```
76
+
77
+ ## 工作原理
78
+
79
+ ### 核心闭环
80
+
81
+ ```
82
+ 用户消息 ──▶ [Injection Transformer] ──▶ 检索相关记忆 ──▶ 注入到 user 消息
83
+ │
84
+ ▼
85
+ [Runtime 运行]
86
+ │
87
+ 助手回复 ──▶ [Capture Extension] ──▶ 要点压缩 ──▶ 存储为记忆 ──▶ 定期合并
88
+ ```
89
+
90
+ ### 注入机制(sentinel)
91
+
92
+ 记忆以 sentinel 包裹块的形式合并进最新 user 消息内容:
93
+
94
+ ```
95
+ <<<AIPACK_MEMORY>>>
96
+ [Relevant memories]
97
+ - 用户偏好 React + TypeScript (score=0.82, id=mem_xxx)
98
+ <<</AIPACK_MEMORY>>
99
+
100
+ <原始用户消息>
101
+ ```
102
+
103
+ 每轮「先剥后注」:注入前先剥除所有 user 消息中的旧 sentinel 块(含已持久化进 session 的),保证当前轮只有一个记忆块。sentinel 是 content 的一部分,随消息持久化,下轮可识别剥离。
104
+
105
+ ### 检索方案
106
+
107
+ | 模式 | 触发条件 | 原理 |
108
+ | ------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------- |
109
+ | 纯 BM25 | 未配置 `embedder`(默认) | 关键词倒排索引,min-max 归一化 |
110
+ | 双路独立召回 | 配置了 `embedder` | BM25 路 + 向量路各自独立召回 top-N,按 id 并集加权融合。向量路走独立 VectorIndex,**不受 BM25 候选池封顶** |
111
+
112
+ 合并(consolidate)阶段使用 `raw` 原始分数模式(不做 min-max 归一化),保证 `similarityThreshold` 按绝对相似度判定。
113
+
114
+ BM25 tokenizer 支持:
115
+
116
+ - **Latin**:小写化 + 按非字母数字分割
117
+ - **CJK**:相邻两字 bigram(区分度远高于单字,如「数据科学」vs「数据库」),奇数长度串尾部补单字保证单字查询可命中;覆盖汉字(含扩展/兼容区)、日文假名、韩文谚文
118
+
119
+ ## 配置选项
120
+
121
+ ### `createMemoryPlugin(options)`
122
+
123
+ | 选项 | 类型 | 默认 | 说明 |
124
+ | ------------------ | ----------------------------- | ------------------------- | -------------------------------------- |
125
+ | `baseDir` | `string` | `<cwd>/.aipack/memory` | FileMemoryStore 存储目录(支持 `~`) |
126
+ | `store` | `MemoryStore` | `FileMemoryStore` | 自定义存储(覆盖默认) |
127
+ | `maxMemories` | `number` | `5` | 每轮注入 top-K |
128
+ | `minScore` | `number` | `0.1` | 最低相关度阈值 |
129
+ | `capture` | `boolean \| CaptureOptions` | `true` | 捕获开关 / 选项 |
130
+ | `inject` | `boolean \| InjectionOptions` | `true` | 注入开关 / 选项 |
131
+ | `tools` | `boolean` | `true` | 记忆工具开关 |
132
+ | `embedder` | `Embedder` | — | 向量化器(启用双路独立召回) |
133
+ | `summarizeFn` | `SummarizeFn` | — | LLM 摘要函数(启用摘要压缩) |
134
+ | `consolidateEvery` | `number` | `0` | 每 N 次捕获自动合并(0=不自动) |
135
+ | `captureTtlMs` | `number` | — | 捕获记忆 TTL(ms),过期后 prune 清理 |
136
+ | `toolTtlMs` | `number` | — | `save_memory` 工具保存的记忆 TTL(ms) |
137
+ | `onEvent` | `MemoryEventSink` | 默认打 warn | 事件接收器(失败/整理/加载等) |
138
+
139
+ ### CaptureOptions
140
+
141
+ | 选项 | 类型 | 默认 | 说明 |
142
+ | ------------------ | ----------------- | ------ | ------------------- |
143
+ | `summarizeFn` | `SummarizeFn` | — | LLM 摘要函数 |
144
+ | `minLength` | `number` | `12` | 最小用户消息长度 |
145
+ | `maxConcepts` | `number` | `8` | 概念数上限 |
146
+ | `maxContentChars` | `number` | `2000` | content 最大字符数 |
147
+ | `consolidateEvery` | `number` | `0` | 每 N 次捕获触发合并 |
148
+ | `ttlMs` | `number` | — | 捕获记忆 TTL(ms) |
149
+ | `onEvent` | `MemoryEventSink` | — | 捕获失败事件接收器 |
150
+
151
+ ## Agent 工具
152
+
153
+ 插件自动注册 4 个 Agent 可调用工具(带输入校验与 limit 裁剪):
154
+
155
+ | 工具 | 参数 | 说明 |
156
+ | --------------- | ---------------------- | ------------------------------------------------------- |
157
+ | `save_memory` | `content`, `concepts?` | 保存一条长期记忆(content ≤ 2000 字,concepts ≤ 20 项) |
158
+ | `search_memory` | `query`, `limit?` | 检索相关记忆(limit ≤ 50) |
159
+ | `list_memories` | `limit?` | 列出最近记忆(limit ≤ 200) |
160
+ | `delete_memory` | `id` | 删除一条记忆 |
161
+
162
+ ## 自定义 Embedder
163
+
164
+ ```typescript
165
+ import { createMemoryPlugin, type Embedder } from 'aipack-memory';
166
+
167
+ // 示例:接入 ollama embedding
168
+ const ollamaEmbedder: Embedder = {
169
+ async embed(text: string): Promise<number[]> {
170
+ const res = await fetch('http://localhost:11434/api/embeddings', {
171
+ method: 'POST',
172
+ headers: { 'Content-Type': 'application/json' },
173
+ body: JSON.stringify({ model: 'nomic-embed-text', prompt: text }),
174
+ });
175
+ const data = await res.json();
176
+ return data.embedding;
177
+ },
178
+ dimension: 768,
179
+ };
180
+
181
+ const mem = createMemoryPlugin({
182
+ embedder: ollamaEmbedder,
183
+ baseDir: '~/.aipack/memory',
184
+ });
185
+ ```
186
+
187
+ ## 自定义 LLM 摘要
188
+
189
+ ```typescript
190
+ import { createMemoryPlugin, type SummarizeFn } from 'aipack-memory';
191
+
192
+ const summarize: SummarizeFn = async ({ userMessage, assistantContent }) => {
193
+ // 调用你的 LLM 压缩对话
194
+ const summary = await callLLM(
195
+ `将以下对话压缩为一句精炼记忆:\n用户: ${userMessage}\n助手: ${assistantContent}`,
196
+ );
197
+ return { summary, concepts: [] };
198
+ };
199
+
200
+ const mem = createMemoryPlugin({
201
+ summarizeFn: summarize,
202
+ });
203
+ ```
204
+
205
+ ## API
206
+
207
+ ### MemoryStore 接口
208
+
209
+ ```typescript
210
+ interface MemoryStore {
211
+ save(entry: MemorySaveInput): Promise<MemoryEntry>; // ttlMs 换算为 expiresAt
212
+ get(id: string): Promise<MemoryEntry | null>;
213
+ delete(id: string): Promise<boolean>;
214
+ list(limit?: number): Promise<MemoryEntry[]>;
215
+ search(query: string, limit?: number): Promise<MemorySearchResult[]>;
216
+ searchVectors(
217
+ queryVec: number[],
218
+ limit?: number,
219
+ ): Promise<MemorySearchResult[]>;
220
+ touchRecall(id: string, at?: number): Promise<void>;
221
+ consolidate(
222
+ options?: ConsolidateOptions,
223
+ ): Promise<{ merged: number; pruned: number }>;
224
+ prune(options?: {
225
+ maxAgeMs?: number;
226
+ minConfidence?: number;
227
+ }): Promise<number>;
228
+ count(): Promise<number>;
229
+ setConsolidator(consolidator: ConsolidatorLike): void;
230
+ markConsolidated(at?: number): void; // 记录合并时间(驱动增量候选窗口)
231
+ stats(): Promise<MemoryStats>; // 统计快照(count/bySource/avgConfidence/recall...)
232
+ dispose(): void; // 释放资源
233
+ }
234
+ ```
235
+
236
+ ### MemoryEntry
237
+
238
+ ```typescript
239
+ interface MemoryEntry {
240
+ id: string;
241
+ content: string;
242
+ concepts: string[];
243
+ confidence: number; // 0..1
244
+ source: 'capture' | 'tool' | 'consolidation';
245
+ sessionKey?: string;
246
+ createdAt: number;
247
+ updatedAt: number; // 仅表示内容修改时间(检索不刷新)
248
+ lastRecalledAt?: number;
249
+ recallCount: number;
250
+ embedding?: number[];
251
+ expiresAt?: number; // TTL 过期时间(save 时 ttlMs 换算)
252
+ meta?: Record<string, unknown>;
253
+ }
254
+ ```
255
+
256
+ ## 限制与注意事项
257
+
258
+ 1. **sentinel 块随会话持久化**:每轮注入前会先剥除历史 sentinel 块,保证当前轮只有一个记忆块。历史 user 消息会被清为原文。
259
+ 2. **并发多会话**:capture 通过 `ExtensionContext.sessionKey`(Runtime 级)与 `beforeRun` 暂存消息配对(框架 per-Runtime 串行)。多会话场景请创建多个 Runtime 实例,各自独立的 sessionKey 互不干扰。
260
+ 3. **内存常驻**:索引(BM25 + 向量)全量常驻内存(零依赖约束下无外部磁盘索引)。百万级记忆需自行评估内存,或按 TTL 控制条数。
261
+ 4. **自定义 store 的混合检索**:自定义 store 需实现 `searchVectors()` 才能启用向量独立召回;未实现时退化为「BM25 候选 + 向量重排」兼容路径。纯 BM25 检索为词法匹配,跨语言同义召回需配置 `embedder`。
262
+ 5. **consolidate 为 best-effort**:增量候选基于 `lastConsolidatedAt`;合并期间新写入的条目留到下一轮处理,跨 id 交错不保证全局原子。
263
+
264
+ ## 验证
265
+
266
+ ```bash
267
+ # 构建
268
+ pnpm --filter aipack build # 先构建框架(peer 依赖)
269
+ pnpm --filter aipack-memory build # 构建插件
270
+
271
+ # 类型检查
272
+ pnpm --filter aipack-memory typecheck
273
+
274
+ # 单元测试(node:test,覆盖 tokenizer/BM25/向量索引/双路检索/合并器/存储/并发)
275
+ pnpm --filter aipack-memory test
276
+
277
+ # 运行往返验证脚本(不依赖真实 LLM / API Key)
278
+ pnpm --filter aipack-memory example
279
+ ```
280
+
281
+ ## License
282
+
283
+ MIT