Files
hermes-agent/docs/2026-09-10_081415-project-history-recall-plugin.md

178 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目历史召回插件设计方案
> **状态:** 设计方案,非实施代码。本文件定义插件的能力边界与集成方式;代码实现需另行授权。
**Goal:** 用一个插件同时解决两个需求:(1) 会话项目归属(project_id),(2) 项目内历史的按需关键词+语义检索。
**定位:** Footprint Ladder 第 4 级 —— 独立插件,不动核心文件,不落 `plugins/memory/`(该目录已关闭新 provider)。装在 `~/.hermes/plugins/project-history-recall/` 或独立仓库 pip 包。
---
## 一、为什么用一个插件而不是两个
两个需求共享同一套底座:会话→项目的归属解析 + state.db 只读访问 + 检索结果格式化。拆成两个插件会重复这份底座并引入"哪个插件负责归属"的模糊边界。一个插件、两个工具,归属逻辑写一次,两个工具都吃它。
**与 V3/V4.1 的关系:** 本插件方案替代它们的"改核心 session_search"路径。V3/V4.1 的归属语义(project_id、移动只改归属、cwd 分离)和检索设计(项目过滤下推、混合检索、溯源)全部保留,但**落点从核心 session_search 改为插件的独立工具**,核心零改动。
## 二、插件结构与发现
```
~/.hermes/plugins/project-history-recall/
├── plugin.yaml # name/version/description,不动核心
├── __init__.py # register(ctx):注册两个工具
├── attribution.py # 归属解析(读 projects.db + sessions 行)
├── search.py # 关键词+语义混合检索
└── config_schema.py # embedding 配置项声明(如有)
```
发现路径:`PluginManager`(`hermes_cli/plugins.py`)扫描 `~/.hermes/plugins/`,later-wins 发现 `register(ctx)`。
注册方式(对齐 `plugins/spotify/__init__.py:28-31` 的现有模式):
```python
def register(ctx) -> None:
ctx.register_tool(name="project_history_search", toolset="project",
schema=SEARCH_SCHEMA, handler=handle_search,
check_fn=check_available, emoji="📂")
ctx.register_tool(name="project_session_move", toolset="project",
schema=MOVE_SCHEMA, handler=handle_move,
check_fn=check_available, emoji="📁")
```
两个工具都在 `project` toolset(会话作用域,桌面端 GUI 平台自动折叠进可用集合,不依赖 `HERMES_DESKTOP` 环境变量)。
## 三、需求一:项目归属(project_id 语义)
**归属数据源:** 复用现有 `projects.db`(`hermes_cli/projects_db.py`)+ 会话行的 `cwd`/`git_repo_root`(`hermes_state_common.py:305-307`)。
**归属解析(attribution.py):** 纯函数,无 IO 写入。
1. 读 `projects.db` 的 `projects` + `project_folders` → 构建文件夹索引(最长祖先匹配,`_FolderIndex` 同款逻辑,插件内重新实现,不从 tui_gateway 复制——它是显示层,不是共享内核)。
2. 当前会话行 → `cwd`/`git_repo_root` → 匹配显式项目文件夹 → `Project.id`。
3. 无显式项目命中 → 用 `git_repo_root` 的 path_key 作为 auto 项目身份(Tier 2)。
4. 无 cwd 且无 git_repo_root → **Home 桶**,返回明确空结果,不检索。
**移动(project_session_move 工具):** 更新会话的归属记录。归属与 cwd 一致移动——会话移到哪个项目,工作目录就跟到哪个项目的目录。通过现有 `session.workspace.move` RPC 完成(它已有持久化 cwd/git_repo_root 的完整路径,见 `tui_gateway/methods_session.py:840-876`),插件在其上叠加归属语义(校验目标项目存在、写入 project_id)。核心零改动。
**归属语义(保留 V3 合同,cwd 跟随移动):**
- 移动会话 = 归属与 cwd 一起切到目标项目目录(workspace.move 的现有行为,插件叠加归属校验)。
- 移动前开始的检索可以完成;移动后的新检索用新归属和新 cwd。
- Home 桶(无 cwd)不检索,返回空并说明原因。
- 跨 profile 归属不合并。
## 四、需求二:项目内历史检索(关键词 + 语义)
**工具:** `project_history_search`,两个模式由 `mode` 参数控制(默认 `hybrid`)。
### 4.1 关键词路径
插件直接只读查询 state.db(`SessionDB` 的只读实例,或 `sqlite3.connect(..., uri=True)` 只读模式):
```
SELECT m.id, m.session_id, m.role, m.content, m.timestamp, s.title, s.started_at
FROM messages m JOIN sessions s ON m.session_id = s.id
WHERE s.<归属匹配条件> AND m.content LIKE / FTS5 MATCH ...
ORDER BY rank / timestamp
LIMIT ?
```
归属匹配条件由 attribution.py 解析出的项目会话集合生成(`s.project_id = ?` 或 `s.cwd LIKE '...%'` 或 `s.git_repo_root = ?`,取决于归属是否已持久化)。
**只读约束是硬性的:** 插件对 state.db 只有 SELECT,绝不 INSERT/UPDATE/DELETE。state.db 是核心的权威存储,插件写入会破坏核心的一致性假设(FTS 索引、token 计数、lineage 状态)。
### 4.2 语义路径
向量索引为插件自有派生数据,存在插件自己的 SQLite 文件(`~/.hermes/plugins/project-history-recall/embeddings.db`)——不碰 state.db schema。
**索引构建:**
- 按归属解析出项目会话集合 → 读 messages 的 user/assistant 文本 → 按消息切片(长消息分段)→ 调用 embedding 模型 → 存向量。
- 首次索引按项目分批执行;后续增量(新消息)在每次检索前检查并补建。
- 索引记录 `session_id + message_id + chunk_offset + content_hash + model_fingerprint + vector`。
- 移动会话后向量不重算——归属解析在查询时重新执行,向量只关联 session_id。
**检索流程:**
1. 归属解析 → 项目会话集合。
2. 关键词:state.db FTS5 查询,限定在集合内。
3. 语义:embeddings.db 向量查询,限定在集合内(先 JOIN 过滤再算相似度,不全库 Top-K 后过滤)。
4. 混合:Reciprocal Rank Fusion 按消息锚点归并两路结果。
5. 展开:对命中消息用 state.db 读原文窗口(前后各 N 条),返回 `session_id + message_id + @session:<id> 链接 + 原文片段`。
**模式:**
- `hybrid`(默认):关键词 + 语义,语义不可用时退化为关键词并注明。
- `keyword`:仅关键词,用于精确符号/错误码查询。
- `semantic`:仅语义,用于不同措辞的相似问题追溯。
### 4.3 结果格式
JSON 字符串返回(对齐现有工具约定):
```json
{
"success": true,
"mode": "hybrid",
"project": {"kind": "explicit", "id": "p_abc", "name": "Hermes"},
"results": [{
"session_id": "20260901_143022_a1b2",
"link": "@session:20260901_143022_a1b2",
"title": "session_search 项目过滤设计",
"message_id": 47,
"snippet": "……项目归属应该持久化在 sessions 表……",
"score_source": "both",
"when": "2026-09-01 14:30"
}],
"sessions_in_scope": 12,
"semantic_status": "ready"
}
```
`semantic_status`:`disabled / consent_required / indexing / ready / partial / unavailable`。
## 五、Embedding 配置与隐私
embedding 模型配置通过插件自己的配置文件(`~/.hermes/plugins/project-history-recall/config.yaml`)或 `config.yaml` 的 `plugins.project-history-recall` 节,走 `hermes tools` 的插件配置 UX。密钥进 `.env`。
**隐私(保留 V4.1 的对话确认设计):**
- 首次需要远程 embedding 且无授权记录时,工具返回 `consent_required` 状态,不发送任何内容。
- Agent 在对话中说明服务方、发送内容、项目范围,用户确认后写入配置。
- 授权持久化,同范围后续不重复询问;换服务/加项目重新确认。
- 未确认时关键词路径正常工作。
## 六、与核心的交互边界
| 插件做的 | 插件不做的 |
|---|---|
| 读 projects.db、读 state.db(只读) | 写 state.db 的 sessions/messages 表(移动除外——workspace.move 有核心自己的写路径) |
| 自己的 embeddings.db(向量存储) | 改 state.db schema |
| 注册两个工具到 `project` toolset | 修改核心 session_search 或 inline executor |
| 归属解析纯函数 | 修改 project_tree.py 或桌面端代码 |
| embedding 调用(自有配置) | 新增核心 HERMES_* 环境变量 |
**核心配合需求:无。** 移动会话归属与 cwd 一致移动,复用现有 `session.workspace.move` RPC 的完整路径(持久化 + git 元数据更新),插件只在其上叠加项目校验和归属语义。核心零改动。
## 七、实施顺序
| 步骤 | 内容 | 验证 |
|---|---|---|
| B0 | 验证 state.db 只读访问 + projects.db 读取 + workspace.move 的归属叠加路径 | 真实临时 HERMES_HOME 下跑通 |
| P1 | attribution.py 归属解析纯函数 + 单测 | 显式项目/自动 repo/Home 桶三态 |
| P2 | project_history_search 关键词路径 + project toolset 注册 | P/Q 项目隔离、Home 桶空结果、溯源链接 |
| P3 | project_session_move 移动 + 归属更新 | 移动后新检索用新归属 |
| P4 | embeddings.db + 语义路径 + 混合检索 | 同义词命中、关键词保底、移动不重算向量 |
| P5 | 对话确认链路 + 隐私控制 | 未确认不发送、确认后自主检索 |
## 八、不做的事
- 不改核心 session_search、inline executor、SessionDB schema。
- 不做项目摘要、会话摘要、自动上下文注入。
- 不做旧会话自动目录归类。
- 不做权限撤销协议、版本表、跨 profile 联合检索。
- 不动全局 MEMORY/USER 的项目隔离。
## 九、评审自审
- **Footprint Ladder:** 第 4 级插件,核心零改动,符合"能力在边缘"。
- **一个插件两个工具:** 归属底座共享,不重复。
- **只读 state.db:** 插件绝不写核心存储,向量索引自有。
- **prompt cache:** 工具结果通道,不碰系统提示词。
- **诚实边界:** 移动时 cwd 跟随归属是已确认的行为(workspace.move 现有路径),无悬而未决项;语义检索的性能包络待 P4 实测,不预先承诺。