# memex **Repository Path**: Hahand/memex ## Basic Information - **Project Name**: memex - **Description**: memex - AI Agent 记忆扩展器:从 Hermes 对话提取结构化事实 → Postgres 图谱存储 → 召回注入。词源:Vannevar Bush 1945《As We May Think》提出的"记忆机器"。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-18 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # memex > **AI Agent 记忆扩展器** —— Hermes `MemoryProvider` 插件,**只说统一协议**;是否请求后端由配置决定,后端自行对接协议。 词源:**Memex** = **Mem**ory + **Ex**tension,源自 Vannevar Bush 1945 年《As We May Think》提出的"个人记忆机器"。 --- ## 这是什么 memex 是 Hermes 的**记忆提供者插件**(`agent.memory_provider.MemoryProvider` 的实现)。 它把「召回 / 落盘 / 压缩前 / 会话边界」这些生命周期钩子,全部翻译成**统一协议**的 6 个端点。**协议层是代码、不是服务** —— 它就是本仓库里的 `protocol/` + `transport/` + `backend/` 三个模块(代码风格解耦),不引入任何独立部署单元、不新开仓库。 ``` Hermes Agent │ MemoryProvider ABC(每轮 prefetch + sync_turn) ↓ memex(本仓库,src/memex/) ├─ provider.py 生命周期钩子、熔断、降级、后端形态分派 ├─ protocol/ 协议模型与 ABC(零业务) ├─ transport/ HTTP + 重试退避(零业务) ├─ backend/ 同一 ABC 的两个实现: │ ├─ ProtocolBackend HTTP 6 端点 → 远端 │ └─ LocalBackend 进程内 → memex.db + lcm.db ├─ matrix/ LCM 只读适配层(LocalBackend 的源数据通道) └─ extract/ 内置语义处理:4 个可插拔执行器 + 本地待抽取队列 │ ├─[remote] HTTP + Bearer(MEMORY_API_URL / MEMORY_API_TOKEN) │ 后端(自行实现 /api/v1/memory/{health,store,recall,…}) └─[local] memex.db fact + 指针(可写) lcm.db 原始对话(只读) cron(每 15 分钟,$HERMES_HOME/scripts/memex_extract.sh) └→ memex extract --once --quiet # 消费本地待抽取队列,产出走 store 直插 ``` **边界**:`local` 形态(默认)在进程内完成全部 6 个操作 —— fact 落 `/memex.db`,原始对话读 `/lcm.db`(只读,写操作在 SQL 层被拒绝)。`remote` 形态只说统一协议,后端必须自己实现这 6 个端点, 插件**不为任何后端写适配器**。两种形态共用同一套协议模型、去重与降级契约 —— 换的是载体,不是理念。 **语义处理(2026-09-28 起)是插件的内置模块,但算力来源可配**: `remote`(默认,外挂给后端在 store 时归纳)/ `rules`(纯规则)/ `session_lm`(借宿主当前会话 LM)/ `http_lm`(插件自主请求,端点与密钥 来自插件自己的配置或 Hermes 的配置)。只有显式启用 `http_lm` 才会带模型 密钥,且密钥只能来自 `$HERMES_HOME/.env` —— 数据库密钥依然一个都不碰。 --- ## 关键设计取舍 | 决策 | 原因 | |---|---| | **运行时零第三方依赖** | 插件在 Hermes 进程内加载。宿主 venv 里**没有** `structlog` / `typer`,引它们会在加载期直接 `ImportError`。所以 HTTP 用 `urllib`、日志用 `logging`、CLI 用 `argparse`。 | | **钩子永不抛异常** | 记忆坏了不能拖垮对话。所有失败降级为「这轮没有记忆」。 | | **热路径不阻塞** | 召回在后台线程做,`prefetch()` 只等一个短超时(默认 3s),超时就放弃注入;`memex_search` 工具作为兜底。 | | **写操作只发生在 primary 上下文** | cron / 子代理的系统提示不是主人的话,写进去等于污染记忆。 | | **插件侧自己做去重** | 按 `content` 去重的后端会对相同内容返回同一个 `memory_id`,但**仍然新增一行原记录**。所以重复提交真的会重复落盘,必须插件侧拦。 | | **空关键词绝不发请求** | 多数召回实现的第一层在关键词集合为空时会命中**全部** active 记录。客户端与 provider 各有一道守卫。 | | **只说统一协议** | 插件内是**代码风格**的协议分层(`protocol/` + `transport/` + `backend/`):6 个端点 + Bearer。是否请求后端由 `MEMORY_BACKEND_ENABLED` 决定;后端必须自己实现协议端点,插件不为任何后端写适配器,代码里搜不到任何后端路径。 | | **两种后端形态,一个 ABC** | `MEMEX_BACKEND_KIND=local`(默认)走进程内实现,`remote` 走 HTTP 统一协议。`provider.py::_build_backend` 是唯一装配点,上层不知道底下是 SQLite 还是 HTTP。 | | **正文不复制(事实少、语料多)** | `local` 形态下 fact 是归纳产物(新内容)落 `memex.db`,原始对话留在 `lcm.db` 只记指针(`src_store_id`)。源库缺席或 schema 不认识时保守落库,不赌别人替我们保存。 | | **写入必带 `occurred_at`** | 召回的新鲜度项只认记录自己的 `occurred_at`。缺它时所有记录分数并列(`score*0.7`),`top_k` 截断会把**刚写的记忆随机丢**在几百条旧记录后面。后端接受写入端传入的 `occurred_at` 并透传到抽取产出的记录(实测:带 → 排第 1,不带 → 排 20 名开外),所以插件每次写入都打当前时刻。 | --- ## 安装 / 部署 memex 是**目录式用户插件**,Hermes 从 `$HERMES_HOME/plugins/` 扫描 (`$HERMES_HOME` 是 Hermes 的家目录,取决于安装方式)。 ```bash # 1. 把插件目录挂到用户的插件扫描根 # $REPO = 本仓库路径;$HERMES_HOME = 含 plugins/ 的 Hermes 目录 ln -sfn "$REPO/src/memex" "$HERMES_HOME/plugins/memex" # 2. 配置插件(可选;默认 local 形态零配置即可工作) # MEMEX_BACKEND_KIND=local # 默认:进程内实现,无需任何 URL/token # 需要接远端时: # MEMEX_BACKEND_KIND=remote # MEMORY_API_URL=<已对接协议的后端地址> # MEMORY_API_TOKEN=<协议 token> # 3. 告诉 Hermes 用哪个记忆提供者 # 在 $HERMES_HOME/config.yaml 里加: # memory: # provider: memex # 4. 验证 cd "$REPO" uv run --quiet --with pytest --with pyyaml python3 -m pytest tests -q -s # 端到端脚本(后端对接协议端点之后才有意义,缺端点时自动 SKIP): # MEMORY_API_URL=... MEMORY_API_TOKEN=... \ # uv run --quiet --with pyyaml python3 scripts/verify/verify_live.py ``` 机密只从 `$HERMES_HOME/.env` 或进程环境变量取 —— 插件**不读任何外部凭据库** (业务凭据与 Hermes 私有密钥域隔离,`MEMEX_CRED_FILE` 机制已移除)。 **注意**:容器里 Hermes 网关启动时快照的 `.env` 可能让子进程继承到旧值, 启用插件前用 `memex status` 核对 `api_url` 与 `backend_enabled`。 --- ## 配置 机密(`$HERMES_HOME/.env` 或环境变量): | 变量 | 默认 | 说明 | |---|---|---| | `MEMORY_API_URL` | 空 | **协议端点**地址:后端自行实现统一协议 6 端点后的入口 | | `MEMORY_API_TOKEN` | 空 | 协议 Bearer token | | `MEMORY_API_TIMEOUT` | `10.0` | HTTP 超时(秒) | 行为参数(环境变量,或 `$HERMES_HOME/memex.json`): | 变量 | 默认 | 说明 | |---|---|---| | `MEMEX_CAPTURE_MODE` | `all` | `all` / `user` / `assistant` / `off` —— 落盘哪些内容 | | `MEMEX_TOP_K` | `5` | 每轮召回条数 | | `MEMEX_MIN_LENGTH` | `5` | 短于此长度的内容不落盘(过滤 "hi"/"ok") | | `MEMEX_SOURCE_TYPE` | `memory` | 写入时附带的 `source_type`(代码默认 `memory`;`.env.example` 里示范了改成 `hermes`) | | `MEMEX_PREFETCH_WAIT` | `3.0` | `prefetch()` 最多等后台召回多久 | | `MEMEX_MAX_CONTENT_CHARS` | `8000` | 单条内容截断上限 | | `MEMEX_MAX_KEYWORDS` | `8` | 每次召回最多送多少个候选关键词 | | `MEMORY_BACKEND_ENABLED` | 未设 = auto | **是否请求后端**:`false` 一律不请求(后端未对接协议时用);`true` 请求但必须 URL+token 齐全;未设 = 配置齐全就请求 | | `MEMORY_FALLBACK` | `builtin` | 后端关闭/不可达时的降级策略:`builtin`(静默降级到 Hermes 内置记忆)/ `none` | | `MEMEX_LM_PLAN` | `event` | **LM 记忆规划触发**:`event`(会话结束+压缩前归纳)/ `store`(仅显式 store 润色)/ `turn`(每轮,费钱)/ `off` | | `MEMEX_LM_PLAN_TIMEOUT` | `20.0` | 规划调用超时(秒) | | `MEMEX_LM_PLAN_MAX_CHARS` | `12000` | 送入规划的对话截断上限 | | `MEMEX_EXTRACT_BACKEND` | `remote` | **语义处理执行器**:`remote`(外挂给后端,方案 A 默认)/ `rules`(纯规则零模型)/ `session_lm`(宿主会话 LM)/ `http_lm`(插件自主请求)/ `auto`(http_lm → session_lm → rules 退级链) | | `MEMEX_EXTRACT_LM_SOURCE` | `plugin` | `http_lm` 的端点配置来源:`plugin`(下面三行)/ `hermes`(读 `$HERMES_HOME/config.yaml` 的 `model:` 段 + `auth.json` 凭据) | | `MEMEX_EXTRACT_LM_URL` | 空 | 自主请求的 OpenAI 兼容端点(如 `http://host:1234/v1`) | | `MEMEX_EXTRACT_LM_KEY` | 空 | 该端点的 API key(只放 `.env`,`memex status` 里打码) | | `MEMEX_EXTRACT_LM_MODEL` | 空 | 模型名 | | `MEMEX_EXTRACT_TIMEOUT` | `30.0` | 抽取调用超时(秒) | | `MEMEX_EXTRACT_QUEUE` | 空 | 本地待抽取队列路径;空 = `$HERMES_HOME/memex-pending.jsonl` | **LM 规划用的是 Hermes 自己的辅助 LM 路由**(`call_llm(task='memory_plan')`)—— 复用当前对话 provider 的模型与鉴权,**不需要任何新增密码/API key**。 想给规划单独指定便宜小模型,在 `config.yaml` 加: ```yaml auxiliary: memory_plan: # memex 注册的 aux task 槽位 provider: opencode-go model: mimo-v2.6-flash ``` 也可以跑 `hermes memory setup` 走配置向导(`get_config_schema()` / `save_config()` 已实现)。 --- ## 语义处理(内置 `extract/` 模块) 抽取能力**在插件里**,但「怎么算」是配置选的执行器 —— 加执行器只加一个文件: | 执行器 | 谁在算 | 何时用 | |---|---|---| | `remote`(默认) | **后端**:原文走协议 `store`,由后端按 `lm_policy` 归纳 | 方案 A 默认;后端自己有抽取链时 | | `rules` | 插件,纯规则启发式(挑带信号的句子 + 捞实体词) | 零模型成本的兜底 | | `session_lm` | 插件调宿主 `call_llm(task='memory_extract')` | 复用当前会话模型,零新密钥 | | `http_lm` | 插件自己发 `POST …/chat/completions` | 想指定端点/便宜模型(端点配置来源 `plugin` 或 `hermes`) | | `auto` | 上面三者退级:http_lm → session_lm → rules | 想"能用什么用什么" | **数据流(本地执行器时)**: 1. `sync_turn()` 把这一轮原文写协议 `store`(`lm_policy=skip`,**后端不再重复抽**) 的同时,追加进本地队列 `$HERMES_HOME/memex-pending.jsonl`; 2. cron 每 15 分钟跑 `memex extract --once --quiet`,认领一批 → 执行器抽取; 3. 产出的每条 `FactDraft` 以 `StoreRequest(level=fact, pre_extracted=…, lm_policy=skip)` 走同一个协议 `store` 落盘(与 `lm_plan` 直插路径共用 L2 契约 v2.1); 4. 失败按 `attempts` 重试,超过上限标 `failed`(原因写在条目 `error` 里); **队列文件本身就是审计日志**,出问题 `memex extract --stats` 一眼看完。 `remote` 模式完全不入队、不消费,插件一个请求都不多发。 --- ## 生命周期钩子 | MemoryProvider 钩子 | memex 行为 | |---|---| | `initialize()` | 抓 `session_id` / `platform` / `agent_context`,建 HTTP 客户端;非 `primary` 上下文标记为只读 | | `on_turn_start()` | 用本轮用户消息**预热**一次后台召回 | | `prefetch()` | 取预热结果 → 注入 `…` 块;超时则放弃 | | `recall_status()` | 上报本轮注入了几条(驱动 UI 的「🧠 recalled N」指示) | | `sync_turn()` | 后台把「用户 + 助手」两个方向写入后端,带 `role`/`session_id`/`platform` 溯源字段;`MEMEX_LM_PLAN=turn` 时先用宿主 LM 规划出记录再**双写** | | `on_session_switch()` | 更新 `session_id`;`reset`/`rewound` 时清空召回缓存与去重签名 | | `on_session_end()` | **event 模式(默认)**:宿主在此钩子跑后台 LLM 任务——把整段对话交给 `call_llm(task='memory_plan')` 归纳成 fact 记录直插(跨轮合并去重),失败静默降级为不写 | | `on_pre_compress()` | **event 模式**:即将被压缩丢弃的上文,在独立线程里归纳成 fact 抢救下来(不阻塞压缩路径);本身仍返回空串、不重复落 raw | | `on_memory_write()` | 把内置 `MEMORY.md` / `USER.md` 的 add/replace 镜像到长期记忆 | | `shutdown()` | join 后台线程 | ### 工具 | 工具 | 作用 | |---|---| | `memory_search` | 召回记忆。`query` 与 `keywords` **二选一**(2026-09-27 起 keywords 可单独用,此前 query 硬必填是 bug);另支持 `top_k` / `mode(precise\|cluster)` / `dimensions` / `time_from` / `time_to` | | `memory_store` | **直接插入归纳好的记录**:`content` + 可选 `tags` / `level(raw\|fact\|summary\|meta, 默认 fact)` / `lm_policy(skip\|auto\|force, 默认 skip)` / `occurred_at`(默认当前 UTC 时刻,用于召回新鲜度) —— 带 `pre_extracted` 走 L2 契约 v2.1,后端可跳过二次 LM 抽取 | | `memory_get` | 按 `memory_id` 取原始记录 | ### 直插已归纳记录:L2 契约 v2.1 `store` 请求体支持三个附加字段。**协议由 memex 定义,后端跟随**: ```json { "content": "主人偏好周五晚上部署", "source_type": "manual", "level": "fact", // raw|fact|summary|meta — 放哪层 "pre_extracted": { // 已归纳好的记录本体 "content": "主人偏好周五晚上部署", "keywords": ["周五", "部署"], "topic": "部署习惯" }, "lm_policy": "skip", // skip=不再抽取 | auto | force "payload": { "trigger": "session_end|pre_compress|turn|manual", ... } } ``` - 插件侧**已按此发送**;忽略未知字段的老后端行为不变 → 向后兼容 - 后端实现后:`level=fact` + `lm_policy=skip` 直写事实层,跳过二次 LM 抽取 #### 后端侧两条附随义务(未实现时 fact 直插「写通读断 / 召回 0 命中」) 1. **`pre_extracted.keywords` 要能被召回命中**:以关键词检索时,后端应把 `pre_extracted.keywords` 解析成关键词关联,而不是要求调用方用原句分词碰运气。 插件侧没有「注册关键词关联」的端点(`link` 是记录间关系,非关键词注册), 所以这一步只能由后端完成;本契约已给出语义(哪些词关联这条记录)。 未实现时:记录写入成功,但按关键词检索恒 0 命中。 2. **`get` 端点必须覆盖 fact 直插路径**:fact 直插返回的 `memory_id` 可能只存在于 事实层的 payload 里,若 `get` 只查原记录层就会 `not found`(写通读断)。 实现:先查原记录,miss 后按 payload 里的 `memory_id` 查事实层,按同一信封返回。 未实现时 `memory_store`(fact) → `memory_get` 这条链会断。 --- ## CLI ```bash memex status # 生效配置(机密打码)+ 后端健康 memex health # 探活协议端点(后端对接后可用) memex search 周五 部署 --top-k 5 # 手工召回 memex store "主人偏好周五晚上部署" # 手工落盘 memex get # 按 ID 取原文 memex extract --stats # 本地待抽取队列状态 memex extract --once [--limit N] # 消费一批(cron 入口带 --quiet 零噪音) ``` --- ## 测试 ```bash # 单元测试(零网络、零数据库) uv run --quiet --with pytest --with pyyaml python3 -m pytest tests -q # 端到端验证(走协议端点;后端缺席时打印 SKIP 并以 0 退出) MEMORY_API_URL=... MEMORY_API_TOKEN=... \ uv run --quiet --with pyyaml python3 scripts/verify/verify_live.py ``` 单元测试覆盖 `config` / `client` / `provider` / `notify` / `extract` 各层的成功与 失败路径,外加 `test_discovery.py` —— 它调用 Hermes **自己**的 `discover_memory_providers()` 与 `load_memory_provider()`,验证插件真的能被宿主发现 和实例化(不是模拟)。宿主安装不存在时该模块整体 skip,绝不伪装成通过。 `scripts/verify/` 下的脚本是验收脚本:它们通过真实 hook 写入、构造记录与关键词 关联打通召回、断言注入块、检查空关键词守卫,最后清理并核对回到基线。 --- ## 对接后端时的已知摩擦点 > 插件只打 6 个协议端点(代码风格协议层,无独立部署单元)。下面是**插件无法 > 自行解决、只能由后端补齐**的几处,是给后端排期用的,不影响插件自身正确性。 1. **抽取要有触发器**。原文落盘后标记了「待归纳」,但没有常驻 worker 就没人认领 (状态一直停在 raw 层),`extract` 类端点需要调用方或定时任务主动调。 **插件侧已有的两条缓解**:`MEMEX_LM_PLAN=event`(会话边界用宿主 LM 直插) 与内置 `extract/` 模块(`MEMEX_EXTRACT_BACKEND` 指定执行器 + 本地队列 + cron)。 后端侧仍需自己的常驻/定时触发。 2. **归纳/去重类端点可能是桩**。有些后端返回「需接入 LM 后启用」,去重归纳并不存在。 3. **关键词注册表**:按关键词检索的第一层通常要求关键词**精确等于**某条记录的 标题字段(实测:包含关键词的子串查询命中 0)。若后端没暴露注册表查询, 调用方只能整句分词碰运气 —— 建议后端在服务端先做注册表解析,或提供 关键词解析/注册端点。 4. **L2 契约 v2.1 三字段待实现**。`level` / `pre_extracted` / `lm_policy` 插件侧 已按上文发送;尚未实现的后端会忽略它们,此时规划出的事实会先落原记录层 再被抽取一遍(重复但无害,语义仍正确)。 5. **响应形态**:不同实现可能返回 `data` 为**对象**(precise 给 `items`、 cluster 给 `groups`)而非列表;本版本在 `_extract_items()` 里统一摊平两种形态。 6. **速率限制**:per-key 频控会让密集调用收到 HTTP 429,调用方需 pacing 与退避。 7. **写入必带 `occurred_at`**(见上文设计取舍)。实测:带 → 排第 1; 不带 → 所有记录分数并列(`score*0.7`),`top_k` 截断会把刚写的记忆**随机丢** 在几百条旧记录后面。另 `ORDER BY score DESC LIMIT k` 同分时次序由执行计划 决定,建议后端加 `created_at DESC` 作次级键。 --- ## 目录结构 ``` memex/ ├── src/memex/ # ← 这个目录本身就是 Hermes 插件目录 │ ├── __init__.py # register(ctx) 出口(模块级零相对导入) │ ├── plugin.yaml # 插件元数据 │ ├── provider.py # MemoryProvider 实现(核心) │ ├── client.py # 统一协议客户端(CLI / 脚本用) │ ├── config.py # 配置合并(env > memex.json) │ ├── protocol/ # 协议模型与 ABC(零业务) │ ├── transport/ # HTTP + 重试退避(零业务) │ ├── backend/ # 同一 ABC 的两个实现 │ │ ├── __init__.py # ProtocolBackend:HTTP 6 端点(远端) │ │ └── local.py # LocalBackend:进程内(memex.db + 指针) │ ├── matrix/ # LCM 只读适配层(LocalBackend 的源数据通道) │ ├── extract/ # 内置语义处理(执行器 + 队列 + 管线) │ │ ├── types.py # FactDraft / 执行器 ABC / local|delegate │ │ ├── parse.py # prompt、JSON 解析、清洗 │ │ ├── rules.py # 纯规则执行器 │ │ ├── session_lm.py # 宿主会话 LM 执行器 │ │ ├── http_lm.py # 自主请求执行器(plugin|hermes 配置来源) │ │ ├── remote.py # 外挂执行器(默认) │ │ ├── queue.py # 本地 JSONL 待抽取队列(flock) │ │ └── pipeline.py # 消费 → 抽取 → 协议落盘 │ ├── notify.py # 通知(可选,永不抛异常) │ ├── logging_setup.py # 结构化日志(stdlib logging 包装) │ └── cli.py # argparse CLI(含 extract 子命令) ├── AGENTS.md # 给 agent 看的仓库约定(目录路由、隐私红线) ├── tests/ # 315 个单元测试 + 宿主发现测试 └── scripts/ ├── verify/ # 验收脚本(走协议端点,SKIP 即缺后端) └── operate/ # 运维脚本(部署/推送等,无业务逻辑) ``` --- ## 许可证 MIT