{"_id":"@aipack-ai/memory","_rev":"7-62eefca1dfc8eb385cf7a40a51277dd3","name":"@aipack-ai/memory","dist-tags":{"latest":"0.0.2"},"versions":{"0.0.1":{"name":"@aipack-ai/memory","version":"0.0.1","keywords":["aipack","memory","agent","bm25","context","retrieval"],"license":"MIT","_id":"@aipack-ai/memory@0.0.1","maintainers":[{"name":"luoguoxiong2021","email":"luoguoxiong2021@163.com"}],"dist":{"shasum":"74602e9c55be5905cf0a33470dbb9252592af4cc","tarball":"https://registry.npmjs.org/@aipack-ai/memory/-/memory-0.0.1.tgz","fileCount":6,"integrity":"sha512-3pLRyyjIaqa883aGyy4h9At3m/RHlvPCiXnJbOF4jl5CCM9LH15SeiLMp28oISUZIRiMHhCFLVqUpXFul7lCOw==","signatures":[{"sig":"MEYCIQDfr9g9sS6XZEAvPn08dX5wyS8gh9I4E83EwTxkZOaC/wIhAOGIWvhxnnckYDDULwQNmLLvsErHzvt+7GcSXTNyxyBX","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":258023},"main":"./dist/index.js","type":"module","_from":"file:aipack-ai-memory-0.0.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"test":"node --import tsx --test tests/*.test.ts","build":"tsup","example":"node --import tsx examples/round-trip.ts","prebuild":"pnpm --filter @aipack-ai/agent build","typecheck":"tsc --noEmit"},"_npmUser":{"name":"luoguoxiong2021","email":"luoguoxiong2021@163.com"},"_resolved":"/tmp/914844d345f736dd81ea79fcbfa79ecb/aipack-ai-memory-0.0.1.tgz","_integrity":"sha512-3pLRyyjIaqa883aGyy4h9At3m/RHlvPCiXnJbOF4jl5CCM9LH15SeiLMp28oISUZIRiMHhCFLVqUpXFul7lCOw==","_npmVersion":"10.8.2","description":"aipack 持久化记忆插件：capture → compress → index → recall/inject → consolidate","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.16.0","tsup":"^8.5.1","typescript":"^5.5.0","@types/node":"^20.12.0","@aipack-ai/agent":"0.0.1"},"peerDependencies":{"@aipack-ai/agent":"0.0.1"},"_npmOperationalInternal":{"tmp":"tmp/memory_0.0.1_1786894254257_0.5982994081172208","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@aipack-ai/memory","version":"0.0.2","description":"aipack 持久化记忆插件：capture → compress → index → recall/inject → consolidate","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"sideEffects":false,"keywords":["aipack","memory","agent","bm25","context","retrieval"],"license":"MIT","engines":{"node":">=18.0.0"},"peerDependencies":{"@aipack-ai/agent":"0.0.2"},"devDependencies":{"@types/node":"^20.12.0","tsup":"^8.5.1","typescript":"^5.5.0","tsx":"^4.16.0","@aipack-ai/agent":"0.0.2"},"scripts":{"build":"tsup","prebuild":"pnpm --filter @aipack-ai/agent build","typecheck":"tsc --noEmit","test":"node --import tsx --test tests/*.test.ts","example":"node --import tsx examples/round-trip.ts"},"_id":"@aipack-ai/memory@0.0.2","_integrity":"sha512-75D4Axx6VuwIopuyE5IGPY/SUK7mKsQnR/18CnhxMfKYL8OIF1AyxrvsGHSUB1x1rBzW4ysbQMB8RJ54nWS25Q==","_resolved":"/tmp/ce725d3c72501e62f247445335fc8168/aipack-ai-memory-0.0.2.tgz","_from":"file:aipack-ai-memory-0.0.2.tgz","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-75D4Axx6VuwIopuyE5IGPY/SUK7mKsQnR/18CnhxMfKYL8OIF1AyxrvsGHSUB1x1rBzW4ysbQMB8RJ54nWS25Q==","shasum":"003e2fa4cb74143b6966406885e070d0b545f55d","tarball":"https://registry.npmjs.org/@aipack-ai/memory/-/memory-0.0.2.tgz","fileCount":6,"unpackedSize":258023,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEFvr/YN6euiHAY2USAxDyE+FQrrnDLqqbNMuLzfBKI4AiEA+UR0lqXt786U6GCqqgdUyRKsOxwWhn9r0TOsa+ayiE0="}]},"_npmUser":{"name":"luoguoxiong2021","email":"luoguoxiong2021@163.com"},"directories":{},"maintainers":[{"name":"luoguoxiong2021","email":"luoguoxiong2021@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/memory_0.0.2_1787062982879_0.09874011335517485"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-16T15:30:53.986Z","modified":"2026-08-18T14:23:03.277Z","1.0.0":"2026-08-16T05:09:10.721Z","1.0.1":"2026-08-16T06:01:54.124Z","2.0.0":"2026-08-16T14:03:42.404Z","0.0.1":"2026-08-16T15:30:54.454Z","0.0.2":"2026-08-18T14:23:03.046Z"},"license":"MIT","keywords":["aipack","memory","agent","bm25","context","retrieval"],"description":"aipack 持久化记忆插件：capture → compress → index → recall/inject → consolidate","maintainers":[{"name":"luoguoxiong2021","email":"luoguoxiong2021@163.com"}],"readme":"# aipack-memory\n\n> aipack 持久化记忆插件：**capture → compress → index → recall/inject → consolidate**\n>\n> 参考 [rohitg00/agentmemory](https://github.com/rohitg00/agentmemory)，为 [aipack](../aipack) 提供「跨会话长期记忆」能力。\n\n## 特性\n\n- **自动捕获**：每轮对话结束自动提取要点存为可检索记忆（零-LLM 要点压缩，可选 LLM 摘要）\n- **自动注入**：每轮对话开始自动检索相关记忆，注入到最新 user 消息（sentinel 机制，防跨轮累积）\n- **BM25 检索**：零依赖关键词检索，支持 CJK（中日韩，bigram）与 Latin 分词\n- **混合检索**：提供 `Embedder` 接口后自动升级为 BM25 + 向量**双路独立召回**融合（向量召回不被 BM25 top-K 封顶）\n- **记忆合并**：增量去重 / 合并相似记忆（O(N²) → 增量窗口），修剪过期与低置信度条目\n- **并发安全**：同 id 写操作经 keyed mutex 串行化，capture 按 sessionKey 配对\n- **可观测性**：`MemoryEvent` 事件上报（失败/整理/加载）与 `stats()` 统计快照\n- **Agent 工具**：4 个可调用工具（save / search / list / delete），带输入校验与可选 TTL\n- **零配置开箱即用**：默认零依赖、零 API Key\n\n## 安装\n\n```bash\npnpm add aipack-memory\n# 或\nnpm install aipack-memory\n```\n\n`aipack` 为 peer 依赖，需同时安装。\n\n## 快速接入\n\n### aipack.config.js\n\n```js\nimport { createMemoryPlugin } from 'aipack-memory';\n\nconst mem = createMemoryPlugin({\n  baseDir: '~/.aipack/memory', // 记忆存储目录\n  maxMemories: 5, // 每轮注入 top-5\n});\n\nconst r = mem.install();\n\nexport default {\n  provider: 'deepseek',\n  model: 'deepseek-v4-flash',\n  systemPrompt: '你是一个有用的助手',\n  sessions: { enabled: true, baseDir: './sessions', maxAge: 30 },\n  extensions: r.extensions,\n  transformers: r.transformers,\n  tools: r.tools,\n};\n```\n\n### 编程式 API\n\n```typescript\nimport { createMemoryPlugin, InMemoryStore } from 'aipack-memory';\n\nconst store = new InMemoryStore();\nconst mem = createMemoryPlugin({ store });\n\n// 直接操作 store\nawait mem.store.save({\n  content: '用户偏好深色主题',\n  concepts: ['ui', 'dark-mode'],\n  confidence: 0.8,\n  source: 'tool',\n});\n\nconst results = await mem.store.search('主题偏好', 5);\nconsole.log(results[0]?.entry.content); // '用户偏好深色主题'\n\n// 手动触发合并\nawait mem.store.consolidate({ similarityThreshold: 0.85 });\n```\n\n## 工作原理\n\n### 核心闭环\n\n```\n用户消息 ──▶ [Injection Transformer]  ──▶ 检索相关记忆 ──▶ 注入到 user 消息\n                                                    │\n                                                    ▼\n                                             [Runtime 运行]\n                                                    │\n助手回复 ──▶ [Capture Extension] ──▶ 要点压缩 ──▶ 存储为记忆 ──▶ 定期合并\n```\n\n### 注入机制（sentinel）\n\n记忆以 sentinel 包裹块的形式合并进最新 user 消息内容：\n\n```\n<<<AIPACK_MEMORY>>>\n[Relevant memories]\n- 用户偏好 React + TypeScript (score=0.82, id=mem_xxx)\n<<</AIPACK_MEMORY>>\n\n<原始用户消息>\n```\n\n每轮「先剥后注」：注入前先剥除所有 user 消息中的旧 sentinel 块（含已持久化进 session 的），保证当前轮只有一个记忆块。sentinel 是 content 的一部分，随消息持久化，下轮可识别剥离。\n\n### 检索方案\n\n| 模式         | 触发条件                  | 原理                                                                                                       |\n| ------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------- |\n| 纯 BM25      | 未配置 `embedder`（默认） | 关键词倒排索引，min-max 归一化                                                                             |\n| 双路独立召回 | 配置了 `embedder`         | BM25 路 + 向量路各自独立召回 top-N，按 id 并集加权融合。向量路走独立 VectorIndex，**不受 BM25 候选池封顶** |\n\n合并（consolidate）阶段使用 `raw` 原始分数模式（不做 min-max 归一化），保证 `similarityThreshold` 按绝对相似度判定。\n\nBM25 tokenizer 支持：\n\n- **Latin**：小写化 + 按非字母数字分割\n- **CJK**：相邻两字 bigram（区分度远高于单字，如「数据科学」vs「数据库」），奇数长度串尾部补单字保证单字查询可命中；覆盖汉字（含扩展/兼容区）、日文假名、韩文谚文\n\n## 配置选项\n\n### `createMemoryPlugin(options)`\n\n| 选项               | 类型                          | 默认                      | 说明                                   |\n| ------------------ | ----------------------------- | ------------------------- | -------------------------------------- |\n| `baseDir`          | `string`                      | `<cwd>/.aipack/memory` | FileMemoryStore 存储目录（支持 `~`）   |\n| `store`            | `MemoryStore`                 | `FileMemoryStore`         | 自定义存储（覆盖默认）                 |\n| `maxMemories`      | `number`                      | `5`                       | 每轮注入 top-K                         |\n| `minScore`         | `number`                      | `0.1`                     | 最低相关度阈值                         |\n| `capture`          | `boolean \\| CaptureOptions`   | `true`                    | 捕获开关 / 选项                        |\n| `inject`           | `boolean \\| InjectionOptions` | `true`                    | 注入开关 / 选项                        |\n| `tools`            | `boolean`                     | `true`                    | 记忆工具开关                           |\n| `embedder`         | `Embedder`                    | —                         | 向量化器（启用双路独立召回）           |\n| `summarizeFn`      | `SummarizeFn`                 | —                         | LLM 摘要函数（启用摘要压缩）           |\n| `consolidateEvery` | `number`                      | `0`                       | 每 N 次捕获自动合并（0=不自动）        |\n| `captureTtlMs`     | `number`                      | —                         | 捕获记忆 TTL（ms），过期后 prune 清理  |\n| `toolTtlMs`        | `number`                      | —                         | `save_memory` 工具保存的记忆 TTL（ms） |\n| `onEvent`          | `MemoryEventSink`             | 默认打 warn               | 事件接收器（失败/整理/加载等）         |\n\n### CaptureOptions\n\n| 选项               | 类型              | 默认   | 说明                |\n| ------------------ | ----------------- | ------ | ------------------- |\n| `summarizeFn`      | `SummarizeFn`     | —      | LLM 摘要函数        |\n| `minLength`        | `number`          | `12`   | 最小用户消息长度    |\n| `maxConcepts`      | `number`          | `8`    | 概念数上限          |\n| `maxContentChars`  | `number`          | `2000` | content 最大字符数  |\n| `consolidateEvery` | `number`          | `0`    | 每 N 次捕获触发合并 |\n| `ttlMs`            | `number`          | —      | 捕获记忆 TTL（ms）  |\n| `onEvent`          | `MemoryEventSink` | —      | 捕获失败事件接收器  |\n\n## Agent 工具\n\n插件自动注册 4 个 Agent 可调用工具（带输入校验与 limit 裁剪）：\n\n| 工具            | 参数                   | 说明                                                    |\n| --------------- | ---------------------- | ------------------------------------------------------- |\n| `save_memory`   | `content`, `concepts?` | 保存一条长期记忆（content ≤ 2000 字，concepts ≤ 20 项） |\n| `search_memory` | `query`, `limit?`      | 检索相关记忆（limit ≤ 50）                              |\n| `list_memories` | `limit?`               | 列出最近记忆（limit ≤ 200）                             |\n| `delete_memory` | `id`                   | 删除一条记忆                                            |\n\n## 自定义 Embedder\n\n```typescript\nimport { createMemoryPlugin, type Embedder } from 'aipack-memory';\n\n// 示例：接入 ollama embedding\nconst ollamaEmbedder: Embedder = {\n  async embed(text: string): Promise<number[]> {\n    const res = await fetch('http://localhost:11434/api/embeddings', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ model: 'nomic-embed-text', prompt: text }),\n    });\n    const data = await res.json();\n    return data.embedding;\n  },\n  dimension: 768,\n};\n\nconst mem = createMemoryPlugin({\n  embedder: ollamaEmbedder,\n  baseDir: '~/.aipack/memory',\n});\n```\n\n## 自定义 LLM 摘要\n\n```typescript\nimport { createMemoryPlugin, type SummarizeFn } from 'aipack-memory';\n\nconst summarize: SummarizeFn = async ({ userMessage, assistantContent }) => {\n  // 调用你的 LLM 压缩对话\n  const summary = await callLLM(\n    `将以下对话压缩为一句精炼记忆：\\n用户: ${userMessage}\\n助手: ${assistantContent}`,\n  );\n  return { summary, concepts: [] };\n};\n\nconst mem = createMemoryPlugin({\n  summarizeFn: summarize,\n});\n```\n\n## API\n\n### MemoryStore 接口\n\n```typescript\ninterface MemoryStore {\n  save(entry: MemorySaveInput): Promise<MemoryEntry>; // ttlMs 换算为 expiresAt\n  get(id: string): Promise<MemoryEntry | null>;\n  delete(id: string): Promise<boolean>;\n  list(limit?: number): Promise<MemoryEntry[]>;\n  search(query: string, limit?: number): Promise<MemorySearchResult[]>;\n  searchVectors(\n    queryVec: number[],\n    limit?: number,\n  ): Promise<MemorySearchResult[]>;\n  touchRecall(id: string, at?: number): Promise<void>;\n  consolidate(\n    options?: ConsolidateOptions,\n  ): Promise<{ merged: number; pruned: number }>;\n  prune(options?: {\n    maxAgeMs?: number;\n    minConfidence?: number;\n  }): Promise<number>;\n  count(): Promise<number>;\n  setConsolidator(consolidator: ConsolidatorLike): void;\n  markConsolidated(at?: number): void; // 记录合并时间（驱动增量候选窗口）\n  stats(): Promise<MemoryStats>; // 统计快照（count/bySource/avgConfidence/recall...）\n  dispose(): void; // 释放资源\n}\n```\n\n### MemoryEntry\n\n```typescript\ninterface MemoryEntry {\n  id: string;\n  content: string;\n  concepts: string[];\n  confidence: number; // 0..1\n  source: 'capture' | 'tool' | 'consolidation';\n  sessionKey?: string;\n  createdAt: number;\n  updatedAt: number; // 仅表示内容修改时间（检索不刷新）\n  lastRecalledAt?: number;\n  recallCount: number;\n  embedding?: number[];\n  expiresAt?: number; // TTL 过期时间（save 时 ttlMs 换算）\n  meta?: Record<string, unknown>;\n}\n```\n\n## 限制与注意事项\n\n1. **sentinel 块随会话持久化**：每轮注入前会先剥除历史 sentinel 块，保证当前轮只有一个记忆块。历史 user 消息会被清为原文。\n2. **并发多会话**：capture 通过 `ExtensionContext.sessionKey`（Runtime 级）与 `beforeRun` 暂存消息配对（框架 per-Runtime 串行）。多会话场景请创建多个 Runtime 实例，各自独立的 sessionKey 互不干扰。\n3. **内存常驻**：索引（BM25 + 向量）全量常驻内存（零依赖约束下无外部磁盘索引）。百万级记忆需自行评估内存，或按 TTL 控制条数。\n4. **自定义 store 的混合检索**：自定义 store 需实现 `searchVectors()` 才能启用向量独立召回；未实现时退化为「BM25 候选 + 向量重排」兼容路径。纯 BM25 检索为词法匹配，跨语言同义召回需配置 `embedder`。\n5. **consolidate 为 best-effort**：增量候选基于 `lastConsolidatedAt`；合并期间新写入的条目留到下一轮处理，跨 id 交错不保证全局原子。\n\n## 验证\n\n```bash\n# 构建\npnpm --filter aipack build          # 先构建框架（peer 依赖）\npnpm --filter aipack-memory build   # 构建插件\n\n# 类型检查\npnpm --filter aipack-memory typecheck\n\n# 单元测试（node:test，覆盖 tokenizer/BM25/向量索引/双路检索/合并器/存储/并发）\npnpm --filter aipack-memory test\n\n# 运行往返验证脚本（不依赖真实 LLM / API Key）\npnpm --filter aipack-memory example\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}