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

9.9 KiB
Raw Permalink Blame History

项目历史召回插件设计方案

状态: 设计方案,非实施代码。本文件定义插件的能力边界与集成方式;代码实现需另行授权。

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 的现有模式):

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 字符串返回(对齐现有工具约定):

{
  "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 实测,不预先承诺。