当前位置: 首页 > news >正文

Hermes Agent 核心原理专题【左扬精讲】—— Memory 子系统全解:ABC 契约 / 8 个内置 provider / 缓存边界

Hermes Agent 核心原理专题【左扬精讲】—— Memory 子系统全解:ABC 契约 / 8 个内置 provider / 缓存边界

这是 Hermes Agent 核心原理专题【左扬精讲】系列 第 8 篇

本篇是 Layer 3 长期记忆层 ── 当用户问 "上次我让你改的那个文件",Hermes 是怎么从 0 到 1 调出 8 个内置 provider(honcho / mem0 / supermemory / byterover / hindsight / holographic / openviking / retaindb)中的一个,又怎么保证注入的 memory 不会破坏 prompt caching的。

第 5、6、7 篇讲了工具怎么注册、怎么编排、命令怎么执行。本篇切到长期记忆层:Hermes 把"记忆持久化 + 跨会话召回"放在 MemoryProvider ABC 后面,只允许1 个外部 provider 跟 builtin 并存 ── 所有上层调用(prefetch_all() / sync_all() / handle_tool_call())拿到的都是同一个 ABC 接口 ── 不需要知道存储是本地 SQLite、Honcho 云端还是 mem0 服务器。

Layer 框架视角:先看 窄腰MemoryProvider ABC + 11 个生命周期方法),再看 编排器MemoryManager 怎么 fan-out / 单线程后台 / 缓存护栏),最后看 边界(prefetch 与 prompt caching 的摩擦点 + 跨 profile 隔离)。

Layer 3 ─ 长期记忆层
════════════════════════════════════════════════════════════Layer 1 ─ Agent 集成点(run_agent.py)AIAgent._memory_manager                        ← 唯一注入点prompt_parts.append(memory_manager.build_system_prompt())context = memory_manager.prefetch_all(user_msg)     ← 每 turn 召回memory_manager.sync_all(user_msg, asst, messages=...)← 后台写memory_manager.queue_prefetch_all(user_msg)          ← 后台预取memory_manager.shutdown_all()                        ← 退出排空Layer 3 ─ 抽象接口(agent/memory_provider.py)MemoryProvider ABC                          ← 窄腰:3 抽象 + 8 默认可选钩子initialize / system_prompt_block            ← 启动期prefetch / queue_prefetch                   ← 召回期sync_turn                                   ← 写入期get_tool_schemas / handle_tool_call         ← 工具期shutdown                                    ← 退出期on_turn_start / on_session_end /            ← 可选钩子on_session_switch / on_pre_compress /on_delegation / on_memory_writeLayer 3 ─ 编排器(agent/memory_manager.py)MemoryManager                               ← 唯一外部访问点add_provider()                              ← 拒绝 2 个外部 providerinitialize_all / build_system_prompt        ← 启动prefetch_all / queue_prefetch_all           ← 召回sync_all                                    ← 后台 ThreadPoolExecutorget_all_tool_schemas / handle_tool_call     ← 工具路由on_turn_start / on_session_end /            ← 可选钩子 fan-outon_session_switch / on_pre_compress /on_delegation / on_memory_writeshutdown_all                                ← 反序 shutdown + 5s drainLayer 3 ─ 8 个内置 provider(plugins/memory/)honcho               HonchoMemoryProvider       ← dialectic Q&A + peer cardsmem0                 Mem0MemoryProvider         ← 服务端 fact extractionsupermemory          SupermemoryMemoryProvider  ← container-tag 容器化byterover            ByteRoverMemoryProvider    ← brv CLI 本地优先hindsight            HindsightMemoryProvider    ← knowledge graph + 多策略holographic          HolographicMemoryProvider  ← SQLite + HRR 检索openviking           OpenVikingMemoryProvider   ← 双向 context DBretaindb             RetainDBMemoryProvider     ← SQLite write queueLayer 3 ─ 插件发现(plugins/memory/__init__.py)_iter_provider_dirs                          ← bundled + user 双扫描discover_memory_providers                    ← (name, desc, available)load_memory_provider(name)                   ← 返回 MemoryProvider 实例_get_active_memory_provider                  ← 读 memory.provider 配置discover_plugin_cli_commands                 ← 只暴露 active provider 的 CLI

Layer 3 MemoryProvider ABC MemoryManager Plugin Discovery Honcho mem0 supermemory byterover hindsight holographic openviking retaindb prompt caching 边界

本篇学习重点

必须掌握

  • 理解 agent/memory_provider.py 中的 MemoryProvider ABC 为什么定义 initialize / prefetch / sync_turn / get_tool_schemas / shutdown 5 个核心方法 + 6 个可选钩子
  • 理解 agent/memory_manager.py 怎么用单 worker ThreadPoolExecutor 把外部 provider 写入后台化,避免 agent 卡"running"几分钟
  • 理解 plugins/memory/__init__.py 怎么发现 / 加载 / 路由 8 个内置 provider,以及为什么 同一时刻只允许 1 个外部 provider
  • 理解 prefetch_all() 与 prompt caching 的关系 ── 召回内容必须可控、可标记、不能污染 system prompt 的稳定前缀
  • 理解 _strip_skill_scaffolding() 为什么先过滤一次再 fan-out ── 防止 skill 展开后的 scaffolding 写入 memory

需要了解

  • 8 个内置 provider 的差异化定位(dialectic / fact extraction / container / knowledge graph / SQLite 等)
  • on_session_switch() 为什么同时支持 resetrewound 两种语义
  • 跨 profile 隔离 ── agent_identity 怎么作为 provider 端 namespace
  • flush_pending() barrier 模式 ── 单 worker executor 怎么保证 turn N 先于 turn N+1 落盘
  • 新 provider 的注册路径 ── bundled 改 plugins/memory/<name>/,user 装到 $HERMES_HOME/plugins/

目录

  • 一、Memory 子系统的位置 ── Layer 3 解决什么?
  • 二、MemoryProvider ABC ── 窄腰与 11 个生命周期方法
  • 三、MemoryManager 编排器 ── 后台化 / 单 provider / 路由表
  • 四、8 个内置 provider 的差异化定位
  • 五、插件发现与 active provider 选择
  • 六、prompt caching 边界 ── prefetch 的可控性
  • 七、FAQ 20 问

一、Memory 子系统的位置 ── Layer 3 解决什么?

Layer 视角 ─ 长期记忆这一层解决什么问题?

前 7 篇覆盖了 Agent 怎么调工具(Layer 1 循环 + Layer 2 编排)和命令怎么执行(Layer 2.5 终端后端)。它们都只关心"当前这一轮":对话上下文、工具调用、本地/远程执行。但用户实际工作时 ── "上次我让你改的那个文件叫什么"、"我三个月前在那个项目里说过的 API 命名" ── 这些信息不在当前 messages 里。

Memory 子系统(Layer 3)解决的就是这个问题:把 turn 边界以外的、跨会话的、长期的事实从对话里抽出来,存到 provider 后端,下次 prefetch 时召回。它跟 Layer 1/2 是完全正交的两个层次 ── Layer 1 不在乎 MemoryProvider 是 honcho 还是 mem0,Layer 3 不在乎 agent 是 CLI / gateway / cron。

Hermes 给出三个互相绑定的设计约束:

  • 窄腰 ABC ── agent/memory_provider.py 只 297 行,定义 MemoryProvider(ABC)(第 42 行),3 个 @abstractmethod + 8 个 concrete (no-op) 默认实现,所有 provider 必须 implement 的只有 3 个方法
  • 编排器单例 ── agent/memory_manager.pyMemoryManager(第 313 行)封装了 fan-out / 后台化 / 路由表 / shutdown 排空,调用方只对 MemoryManager 不对 provider 数组
  • 1 个外部 provider 限制 ── MemoryManager.add_provider()(第 334 行)只允许 builtin + 至多 1 个 external,避免 schema 膨胀和 backend 冲突

1.1 全景:8 个内置 provider + MemoryManager 的位置

概览:

Hermes 当前内置 8 个 MemoryProvider 实现,全部放在 plugins/memory/,由 plugins/memory/__init__.py 通过 bundled 扫描发现。它们差异化的是存储后端(Honcho 云服务 / mem0 REST / 本地 SQLite / 知识图谱等)和召回策略(dialectic Q&A / container-tag / 多策略 hybrid),共享的是同一个 ABC 契约。

定位:

本篇核心源码文件 ──

  • agent/memory_provider.py(234 行)── MemoryProvider ABC
  • agent/memory_manager.py(822 行)── MemoryManager 编排器
  • plugins/memory/__init__.py(371 行)── provider 发现 + 加载
  • plugins/memory/<name>/__init__.py × 8 ── 各 provider 实现

run_agent.py 中的唯一注入点 ── AIAgent._memory_manager = MemoryManager()(module 文档第 11 行描述)。

1.2 Memory 子系统与 Layer 1/2 的边界

理由:

为什么 Memory 是独立 Layer 而不是 Tool 的一部分?

  • Tool 是"当前 turn 的请求-响应" ── terminal.run()search_files()delegate_task() 都是即时执行的,不带跨 turn 的状态机
  • Memory 是"跨 turn 的持续状态" ── prefetch() 在每 turn 之前召回历史事实,sync_turn() 在每 turn 之后把新事实写入后端,这两个调用本身就是生命周期,不是请求
  • Provider 暴露的 tool 是特例 ── 某些 provider(honcho / mem0 / supermemory)让 agent 在 system prompt 中看到 search / recall 工具主动调用,这些 tool 由 MemoryManager 路由(不是 model_tools.handle_function_call()

具体边界 ── MemoryManager.handle_tool_call()agent/memory_manager.py 第 682 行)独立于 tools/registry.py 的核心 tool 路由,两套路由表并存_HERMES_CORE_TOOLS vs provider tool schemas)。

本节小结

  • Memory 子系统(Layer 3)解决 跨 turn / 跨会话 的事实持久化和召回
  • agent/memory_provider.py 第 42 行的 ABC 定义了 3 个 abstract + 8 个 concrete 默认,强制实现的成本最低
  • agent/memory_manager.py 第 313 行的编排器把 fan-out / 后台化 / shutdown 收拢到一个入口
  • 与 Tool 系统正交 ── Provider tool 走 MemoryManager.handle_tool_call(),不走 tools/registry.py

二、MemoryProvider ABC ── 窄腰与 11 个生命周期方法

Layer 视角 ─ ABC 这一层解决什么?

一个 MemoryProvider 后端在概念上要做 4 件事:启动时初始化每 turn 之前召回每 turn 之后写入退出时清理。再加 1 个可选工具暴露(让模型主动 query),就是 5 个核心生命周期方法。Hermes 把这 5 个方法的签名定死在 ABC 上,把 6 个更细粒度的钩子(on_turn_start / on_session_end / on_session_switch / on_pre_compress / on_delegation / on_memory_write)做成 concrete (no-op) 默认实现 ── provider 想用就覆写,不想用就继承默认。

结果:强制实现的成本只有 3 个 abstractmethodname / is_available / initialize / get_tool_schemas),其他全部是默认 return "" / pass。一个最小化的 MemoryProvider 可以只覆写这 5 个核心方法 ── 80 行代码就能跑起来(honcho / mem0 都是这么做的)。

2.1 5 个核心生命周期方法

契约:

agent/memory_provider.py 第 42 行 class MemoryProvider(ABC) 的 5 个核心方法签名:

  1. name第 45-48 行@property @abstractmethod)── def name(self) -> str,短 id 如 "builtin" / "honcho"
  2. is_available第 52-58 行@abstractmethod)── def is_available(self) -> bool,检查 config / credentials,不允许网络调用
  3. initialize第 60-82 行@abstractmethod)── def initialize(self, session_id: str, **kwargs) -> Nonekwargs 必含 hermes_home / platform,可选 agent_context / agent_identity / agent_workspace / parent_session_id / user_id / user_id_alt
  4. prefetch第 93-105 行concrete default return "")── def prefetch(self, query: str, *, session_id: str = "") -> str,每 turn 之前调用,要快(实际 fetch 走后台线程)
  5. queue_prefetch第 107-113 行concrete no-op)── 后台预取,结果缓存到下一次 prefetch()
  6. sync_turn第 115-131 行concrete no-op)── def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None) -> None,每 turn 之后调用,非阻塞
  7. get_tool_schemas第 133-141 行@abstractmethod)── def get_tool_schemas(self) -> List[Dict[str, Any]],返回 OpenAI function calling 格式
  8. handle_tool_call第 143-149 行concrete raises NotImplementedError)── 路由 provider 自有 tool,返回 JSON 字符串
  9. shutdown第 151-152 行concrete no-op)── flush queue + close connection

关键约束:

  • is_available() 不能网络调用 ── 只读 config / 安装依赖,避免 agent 启动时被网络卡住
  • sync_turn() 必须非阻塞 ── MemoryManager 把它 dispatch 到后台线程(agent/memory_manager.py 第 575 行 _submit_background()),provider 自己再 queue 到本地的 executor 才是合规的
  • get_tool_schemas() 必须返回 OpenAI 标准 schema ── {"name": "...", "description": "...", "parameters": {...}} 格式,由 MemoryManager.get_all_tool_schemas() 第 645 行拼到 agent 的 tool 列表

2.2 6 个可选生命周期钩子

契约:

6 个可选钩子的签名(全部 concrete default,provider 不实现就走默认 no-op / return ""):

  1. on_turn_start(turn_number, message, **kwargs)第 156-163 行)── 每 turn 开始,kwargs 可能含 remaining_tokens / model / platform / tool_count
  2. on_session_end(messages)第 165-173 行)── 会话真正结束(CLI exit / /reset / gateway 过期),不是每 turn
  3. on_session_switch(new_session_id, *, parent_session_id="", reset=False, rewound=False, **kwargs)第 175-217 行)── /resume / /branch / /reset / /new + context compression 触发
  4. on_pre_compress(messages) -> str第 219-229 行)── 上下文压缩前,返回要注入压缩 summary 的额外内容
  5. on_delegation(task, result, *, child_session_id="", **kwargs)第 231-242 行)── 仅在父 agent 上调用,subagent 不持 providerskip_memory=True
  6. on_memory_write(action, target, content, metadata=None)第 279-296 行)── builtin memory tool 写入时 mirror 到外部 provider

关键约束:

  • on_session_switch()reset vs rewound 区别 ── reset=True 表示全新会话(/reset / /new),provider 应清空 per-session buffer;rewound=True 表示 session_id 不变但 transcript 被截断(/undo),只 invalidate per-turn document state
  • on_delegation() 永远在 parent 调 ── subagent 自己没 provider(run_agent.py 给 subagent 传 skip_memory=True),parent 观察到 task + result pair

2.3 setup wizard 与 native config

契约:

两个 config 适配方法(agent/memory_provider.py):

  • get_config_schema()第 244-260 行)── 返回 hermes memory setup 向导字段列表,每个字段含 key / description / secret / required / default / choices / url / env_var
  • save_config(values, hermes_home)第 262-277 行)── 把非 secret 字段写到 provider 自己的 native config 路径

关键约束(第 273-277 行强制声明):

  • 每个新 provider 必须实现其中一个 ── 要么 save_config() 写到 native 格式(如 honcho 写 $HERMES_HOME/honcho.json,见 plugins/memory/honcho/__init__.py 第 257 行),要么只用 env var(get_config_schema() 全填 env_var

细节:

agent/memory_provider.py 第 42-152 行的 ABC 完整声明(精简):

class MemoryProvider(ABC):"""Abstract base class for memory providers."""# -- Identity (implement) --@property@abstractmethoddef name(self) -> str: ...@abstractmethoddef is_available(self) -> bool: ...# -- Startup (implement) --@abstractmethoddef initialize(self, session_id: str, **kwargs) -> None: ...# -- Recall (default no-op) --def system_prompt_block(self) -> str:return ""def prefetch(self, query: str, *, session_id: str = "") -> str:return ""def queue_prefetch(self, query: str, *, session_id: str = "") -> None:pass  # override for background prefetch# -- Write (default no-op) --def sync_turn(self, user_content, assistant_content, *,session_id: str = "",messages: Optional[List[Dict[str, Any]]] = None,) -> None:pass  # override for non-blocking persist# -- Tools (implement) --@abstractmethoddef get_tool_schemas(self) -> List[Dict[str, Any]]: ...def handle_tool_call(self, tool_name, args, **kwargs) -> str:raise NotImplementedError(...)# -- Teardown (default no-op) --def shutdown(self) -> None:pass

本节小结

  • MemoryProvider(ABC)(第 42 行)定义 3 个 abstractmethod + 11 个 concrete 默认 ── 强制实现的成本只有 name / is_available / initialize / get_tool_schemas 共 4 个
  • 核心生命周期:initialize 启动 / prefetch 召回 / sync_turn 写 / get_tool_schemas 暴露 / shutdown 清理
  • 可选钩子:on_turn_start / on_session_end / on_session_switch / on_pre_compress / on_delegation / on_memory_write ── 全默认 no-op / return ""
  • 配置契约:get_config_schema / save_config ── 二选一(native 格式或纯 env var)

三、MemoryManager 编排器 ── 后台化 / 单 provider / 路由表

Layer 视角 ─ 编排器这一层解决什么?

ABC 只定义了"每个 provider 应该能做什么",但没有回答"调用方怎么用 N 个 provider"。如果直接让 run_agent.py 维护一个 list[MemoryProvider] 并手动 fan-out,会立刻撞到 4 个问题:

  1. 背景化 ── sync_turn() 可能卡 298s(Hindsight daemon 配置错误的真实案例,agent/memory_manager.py 第 528 行 docstring),inline 调用会让 agent 一直标 "running"
  2. 单 provider 限制 ── 同时激活 2 个 external provider 会导致 tool schema 膨胀和写入冲突
  3. 核心 tool 名保留 ── provider 不能声明叫 clarify / delegate_task 的 tool shadow core
  4. Turn 顺序保证 ── turn N 必须先于 turn N+1 落盘,否则 recall 会拿到旧数据

agent/memory_manager.pyMemoryManager第 313 行)把这 4 件事一起收拢:

  • add_provider() 拒绝 2 个 external + 拒绝 shadow core tool name
  • sync_all() / queue_prefetch_all() 通过单 worker ThreadPoolExecutor 串行化后台执行
  • _tool_to_provider 字典路由 provider tool call
  • flush_pending() 用 sentinel future 实现 barrier

3.1 __init__ 与 add_provider ── 单外部 provider 强制

定位:

agent/memory_manager.py

  • class MemoryManager ── 第 313 行
  • __init__ ── 第 320 行(4 个字段:_providers / _tool_to_provider / _has_external / _sync_executor
  • add_provider(provider) ── 第 334 行
  • build_system_prompt() ── 第 413 行
  • prefetch_all() ── 第 452 行
  • queue_prefetch_all() ── 第 474 行
  • sync_all() ── 第 515 行
  • _submit_background() ── 第 575 行
  • _get_sync_executor() ── 第 604 行
  • flush_pending() ── 第 620 行
  • get_all_tool_schemas() ── 第 645 行
  • get_all_tool_names() ── 第 674 行
  • has_tool() ── 第 678 行
  • handle_tool_call() ── 第 682 行
  • on_turn_start() ── 第 704 行
  • on_session_end() ── 第 718 行
  • on_session_switch() ── 第 729 行
  • on_pre_compress() ── 第 777 行
  • on_memory_write() ── 第 822 行
  • on_delegation() ── 第 852 行
  • shutdown_all() ── 第 866 行
  • _drain_sync_executor() ── 第 885 行
  • initialize_all() ── 第 931 行

源码视角 ─ add_provider() 关键路径(agent/memory_manager.py 第 334-397 行):

def add_provider(self, provider: MemoryProvider) -> None:is_builtin = provider.name == "builtin"if not is_builtin:if self._has_external:existing = next((p.name for p in self._providers if p.name != "builtin"), "unknown")logger.warning("Rejected memory provider '%s' — external provider '%s' is ""already registered. Only one external memory provider is ""allowed at a time. Configure which one via memory.provider ""in config.yaml.",provider.name, existing,)returnself._has_external = Trueself._providers.append(provider)# Core tool names are reserved — reject shadowingfrom toolsets import _HERMES_CORE_TOOLS_core_tool_names = set(_HERMES_CORE_TOOLS)for schema in provider.get_tool_schemas():tool_name = schema.get("name", "")if tool_name in _core_tool_names:logger.warning("Memory provider '%s' tool '%s' shadows a reserved core ""tool name; registration ignored. Core tools always win — ""rename the provider's tool to something unique.",provider.name, tool_name,)continueif tool_name and tool_name not in self._tool_to_provider:self._tool_to_provider[tool_name] = providerelif tool_name in self._tool_to_provider:logger.warning("Memory tool name conflict: '%s' already ...", tool_name, ...)

关键设计:

  • builtin 永远先入 ── is_builtin = provider.name == "builtin"(第 341 行),builtin 不算 external 不受单 provider 限制
  • 第 2 个 external 被 reject + 警告 ── 不抛异常,避免 provider 安装顺序错乱时整个 agent 起不来;只是 warning log
  • Shadow core tool 在路由前就拒绝 ── _HERMES_CORE_TOOLS 名单(clarify / delegate_task / memory 等),provider tool name 重了就 不进入路由表continue 而非 append)
  • _tool_to_provider 用 first-wins ── 第 2 个 provider 声明同名 tool 会被警告并忽略

3.2 sync_all / queue_prefetch_all ── 单 worker 后台化

源码视角 ─ sync_all() 与后台执行器(agent/memory_manager.py 第 515-617 行):

def sync_all(self, user_content, assistant_content, *,session_id="", messages=None) -> None:"""Sync a completed turn to all providers.Runs on a background worker thread, NOT inline on the turn-completionpath. A provider's sync_turn may make a blocking network/daemon call(a misconfigured Hindsight daemon was observed blocking ~298s beforefailing); doing that inline held run_conversation open long afterthe user saw their response..."""providers = list(self._providers)if not providers:returnclean_user_content = self._strip_skill_scaffolding(user_content)if not clean_user_content:returnuser_content = clean_user_contentdef _run() -> None:for provider in providers:try:if messages is not None and self._provider_sync_accepts_messages(provider):provider.sync_turn(user_content, assistant_content,session_id=session_id, messages=messages)else:provider.sync_turn(user_content, assistant_content,session_id=session_id)except Exception as e:logger.warning("Memory provider '%s' sync_turn failed: %s", provider.name, e)self._submit_background(_run)def _get_sync_executor(self) -> Optional[ThreadPoolExecutor]:"""Lazily create the single-worker background executor."""if self._sync_executor is not None:return self._sync_executorwith self._sync_executor_lock:if self._sync_executor is None:try:self._sync_executor = ThreadPoolExecutor(max_workers=1,thread_name_prefix="mem-sync",)except Exception as e:logger.warning("Failed to create memory sync executor: %s", e)return Nonereturn self._sync_executor

关键设计:

  • 单 worker = 顺序保证 ── max_workers=1(第 612 行)确保 turn N 必然先于 turn N+1 落盘;provider 自己不需要写重试/排序逻辑
  • Lazily created ── 第 606 行双检锁,纯 builtin 路径根本不起线程,零开销
  • 守护线程 ── thread_name_prefix="mem-sync"(第 613 行)+ Python 默认 daemon=True,进程退出时即使还有 sync 没跑完也会被 kill(不会阻塞 exit)
  • signature 检查回退 ── _provider_sync_accepts_messages()(第 503 行)用 inspect.signature() 判断 provider 是否接受 messages kwarg,老 provider 无需改

3.3 _strip_skill_scaffolding ── 防止 skill 展开污染

理由:

MemoryManager._strip_skill_scaffolding()agent/memory_manager.py 第 434-450 行)是一个关键的护栏。当用户调 /skill/bundle,Hermes 把整个 SKILL.md body 嵌入到 model-facing message 里 ── 直接把这条 message 喂给 memory provider 会让 store / embedding 里充满"prompt scaffolding"而不是用户真实意图。

该方法(注释第 442-449 行说明)委托给 agent/skill_commands.pyextract_user_instruction_from_skill_message(),从 skill 展开后的 turn 里恢复出用户的原始指令。规则:

  • 非 skill message 原样透传
  • Skill turn 含 user instruction ── 返回那段 instruction
  • Bare skill invocation(无 instruction)── 返回 Nonecaller 跳过整轮

sync_all()prefetch_all() 都先过这层过滤(第 458、544 行),一次性、整个 fan-out 共享,避免每个 provider 各自实现一遍

3.4 flush_pending ── 单 executor barrier

契约:

flush_pending(timeout: Optional[float] = None) -> boolagent/memory_manager.py 第 620-641 行):

def flush_pending(self, timeout=None) -> bool:"""Block until queued sync/prefetch work has drained.Single-worker executor means submitting a sentinel and waiting onit guarantees every previously-submitted task has run. Returns Trueif the barrier completed within timeout (or no executor exists),False on timeout. Used at real session boundaries and by tests thatneed to assert provider state deterministically."""executor = self._sync_executorif executor is None:return Truetry:fut = executor.submit(lambda: None)except RuntimeError:return True  # executor already shut down — nothing pendingtry:fut.result(timeout=timeout)return Trueexcept Exception:return False

关键设计:

  • 单 worker 的性质利用 ── executor.submit(lambda: None) 排队一个 sentinel,由于单 worker 串行,sentinel 之前的所有 task 必然都已完成
  • timeout 而非 shutdown(wait=True) ── 后者会永久阻塞,单点拖住整个 session shutdown
  • 真正会话边界 + 测试都调 ── docstring 第 624-627 行说明

3.5 shutdown_all ── 反序 + 5s drain

行为:

shutdown_all()agent/memory_manager.py 第 866-883 行) + _drain_sync_executor()第 885-922 行)的关闭顺序:

  1. _drain_sync_executor() ── executor.shutdown(wait=False, cancel_futures=True) 停止接受新任务 + 取消未启动的;启动一个 mem-sync-drain 守护线程 wait 已经 in-flight 的任务最多 _SYNC_DRAIN_TIMEOUT_S=5.0(第 922 行)
  2. 反序遍历 self._providersprovider.shutdown() ── 后注册的先关(external provider 大多依赖 builtin 的 snapshot,先关 external 才能保证 builtin snapshot 还在)

关键设计:

  • 5s 上限来自 docstring 第 41-44 行 ── "A wedged provider must never block process teardown indefinitely — the worker threads are daemon, so anything still running past this window dies with the interpreter."
  • cancel_futures=True 兼容处理 ── 第 904 行 except TypeError 兜底老 Python(cancel_futures 是 3.9+ 才有)

本节小结

  • MemoryManager(第 313 行)封装 fan-out / 后台化 / 路由表 / shutdown 排空
  • 单 worker ThreadPoolExecutormax_workers=1)保证 turn 顺序,daemon 线程避免阻塞 exit
  • add_provider() 强制 1 个外部 provider + 拒绝 shadow core tool
  • _strip_skill_scaffolding() 在 fan-out 前过滤 skill 展开污染
  • flush_pending() 用 sentinel barrier + timeout 避免永久阻塞

三、MemoryManager 编排器 ── 后台化 / 单 provider / 路由表

Layer 视角 ─ 编排器这一层解决什么?

ABC 只定义了"每个 provider 应该能做什么",但没有回答"调用方怎么用 N 个 provider"。如果直接让 run_agent.py 维护一个 list[MemoryProvider] 并手动 fan-out,会立刻撞到 4 个问题:

  1. 背景化 ── sync_turn() 可能卡 298s(Hindsight daemon 配置错误的真实案例,agent/memory_manager.py 第 528 行 docstring),inline 调用会让 agent 一直标 "running"
  2. 单 provider 限制 ── 同时激活 2 个 external provider 会导致 tool schema 膨胀和写入冲突
  3. 核心 tool 名保留 ── provider 不能声明叫 clarify / delegate_task 的 tool shadow core
  4. Turn 顺序保证 ── turn N 必须先于 turn N+1 落盘,否则 recall 会拿到旧数据

agent/memory_manager.pyMemoryManager第 313 行)把这 4 件事一起收拢:

  • add_provider() 拒绝 2 个 external + 拒绝 shadow core tool name
  • sync_all() / queue_prefetch_all() 通过单 worker ThreadPoolExecutor 串行化后台执行
  • _tool_to_provider 字典路由 provider tool call
  • flush_pending() 用 sentinel future 实现 barrier

3.1 __init__ 与 add_provider ── 单外部 provider 强制

定位:

agent/memory_manager.py

  • class MemoryManager ── 第 313 行
  • __init__ ── 第 320 行(4 个字段:_providers / _tool_to_provider / _has_external / _sync_executor
  • add_provider(provider) ── 第 334 行
  • build_system_prompt() ── 第 413 行
  • prefetch_all() ── 第 452 行
  • queue_prefetch_all() ── 第 474 行
  • sync_all() ── 第 515 行
  • _submit_background() ── 第 575 行
  • _get_sync_executor() ── 第 604 行
  • flush_pending() ── 第 620 行
  • get_all_tool_schemas() ── 第 645 行
  • get_all_tool_names() ── 第 674 行
  • has_tool() ── 第 678 行
  • handle_tool_call() ── 第 682 行
  • on_turn_start() ── 第 704 行
  • on_session_end() ── 第 718 行
  • on_session_switch() ── 第 729 行
  • on_pre_compress() ── 第 777 行
  • on_memory_write() ── 第 822 行
  • on_delegation() ── 第 852 行
  • shutdown_all() ── 第 866 行
  • _drain_sync_executor() ── 第 885 行
  • initialize_all() ── 第 931 行

源码视角 ─ add_provider() 关键路径(agent/memory_manager.py 第 334-397 行):

def add_provider(self, provider: MemoryProvider) -> None:is_builtin = provider.name == "builtin"if not is_builtin:if self._has_external:existing = next((p.name for p in self._providers if p.name != "builtin"), "unknown")logger.warning("Rejected memory provider '%s' — external provider '%s' is ""already registered. Only one external memory provider is ""allowed at a time. Configure which one via memory.provider ""in config.yaml.",provider.name, existing,)returnself._has_external = Trueself._providers.append(provider)# Core tool names are reserved — reject shadowingfrom toolsets import _HERMES_CORE_TOOLS_core_tool_names = set(_HERMES_CORE_TOOLS)for schema in provider.get_tool_schemas():tool_name = schema.get("name", "")if tool_name in _core_tool_names:logger.warning("Memory provider '%s' tool '%s' shadows a reserved core ""tool name; registration ignored. Core tools always win — ""rename the provider's tool to something unique.",provider.name, tool_name,)continueif tool_name and tool_name not in self._tool_to_provider:self._tool_to_provider[tool_name] = providerelif tool_name in self._tool_to_provider:logger.warning("Memory tool name conflict: '%s' already ...", tool_name, ...)

关键设计:

  • builtin 永远先入 ── is_builtin = provider.name == "builtin"(第 341 行),builtin 不算 external 不受单 provider 限制
  • 第 2 个 external 被 reject + 警告 ── 不抛异常,避免 provider 安装顺序错乱时整个 agent 起不来;只是 warning log
  • Shadow core tool 在路由前就拒绝 ── _HERMES_CORE_TOOLS 名单(clarify / delegate_task / memory 等),provider tool name 重了就 不进入路由表continue 而非 append)
  • _tool_to_provider 用 first-wins ── 第 2 个 provider 声明同名 tool 会被警告并忽略

3.2 sync_all / queue_prefetch_all ── 单 worker 后台化

源码视角 ─ sync_all() 与后台执行器(agent/memory_manager.py 第 515-617 行):

def sync_all(self, user_content, assistant_content, *,session_id="", messages=None) -> None:"""Sync a completed turn to all providers.Runs on a background worker thread, NOT inline on the turn-completionpath. A provider's sync_turn may make a blocking network/daemon call(a misconfigured Hindsight daemon was observed blocking ~298s beforefailing); doing that inline held run_conversation open long afterthe user saw their response..."""providers = list(self._providers)if not providers:returnclean_user_content = self._strip_skill_scaffolding(user_content)if not clean_user_content:returnuser_content = clean_user_contentdef _run() -> None:for provider in providers:try:if messages is not None and self._provider_sync_accepts_messages(provider):provider.sync_turn(user_content, assistant_content,session_id=session_id, messages=messages)else:provider.sync_turn(user_content, assistant_content,session_id=session_id)except Exception as e:logger.warning("Memory provider '%s' sync_turn failed: %s", provider.name, e)self._submit_background(_run)def _get_sync_executor(self) -> Optional[ThreadPoolExecutor]:"""Lazily create the single-worker background executor."""if self._sync_executor is not None:return self._sync_executorwith self._sync_executor_lock:if self._sync_executor is None:try:self._sync_executor = ThreadPoolExecutor(max_workers=1,thread_name_prefix="mem-sync",)except Exception as e:logger.warning("Failed to create memory sync executor: %s", e)return Nonereturn self._sync_executor

关键设计:

  • 单 worker = 顺序保证 ── max_workers=1(第 612 行)确保 turn N 必然先于 turn N+1 落盘;provider 自己不需要写重试/排序逻辑
  • Lazily created ── 第 606 行双检锁,纯 builtin 路径根本不起线程,零开销
  • 守护线程 ── thread_name_prefix="mem-sync"(第 613 行)+ Python 默认 daemon=True,进程退出时即使还有 sync 没跑完也会被 kill(不会阻塞 exit)
  • signature 检查回退 ── _provider_sync_accepts_messages()(第 503 行)用 inspect.signature() 判断 provider 是否接受 messages kwarg,老 provider 无需改

3.3 _strip_skill_scaffolding ── 防止 skill 展开污染

理由:

MemoryManager._strip_skill_scaffolding()agent/memory_manager.py 第 434-450 行)是一个关键的护栏。当用户调 /skill/bundle,Hermes 把整个 SKILL.md body 嵌入到 model-facing message 里 ── 直接把这条 message 喂给 memory provider 会让 store / embedding 里充满"prompt scaffolding"而不是用户真实意图。

该方法(注释第 442-449 行说明)委托给 agent/skill_commands.pyextract_user_instruction_from_skill_message(),从 skill 展开后的 turn 里恢复出用户的原始指令。规则:

  • 非 skill message 原样透传
  • Skill turn 含 user instruction ── 返回那段 instruction
  • Bare skill invocation(无 instruction)── 返回 Nonecaller 跳过整轮

sync_all()prefetch_all() 都先过这层过滤(第 458、544 行),一次性、整个 fan-out 共享,避免每个 provider 各自实现一遍

3.4 flush_pending ── 单 executor barrier

契约:

flush_pending(timeout: Optional[float] = None) -> boolagent/memory_manager.py 第 620-641 行):

def flush_pending(self, timeout=None) -> bool:"""Block until queued sync/prefetch work has drained.Single-worker executor means submitting a sentinel and waiting onit guarantees every previously-submitted task has run. Returns Trueif the barrier completed within timeout (or no executor exists),False on timeout. Used at real session boundaries and by tests thatneed to assert provider state deterministically."""executor = self._sync_executorif executor is None:return Truetry:fut = executor.submit(lambda: None)except RuntimeError:return True  # executor already shut down — nothing pendingtry:fut.result(timeout=timeout)return Trueexcept Exception:return False

关键设计:

  • 单 worker 的性质利用 ── executor.submit(lambda: None) 排队一个 sentinel,由于单 worker 串行,sentinel 之前的所有 task 必然都已完成
  • timeout 而非 shutdown(wait=True) ── 后者会永久阻塞,单点拖住整个 session shutdown
  • 真正会话边界 + 测试都调 ── docstring 第 624-627 行说明

3.5 shutdown_all ── 反序 + 5s drain

行为:

shutdown_all()agent/memory_manager.py 第 866-883 行) + _drain_sync_executor()第 885-922 行)的关闭顺序:

  1. _drain_sync_executor() ── executor.shutdown(wait=False, cancel_futures=True) 停止接受新任务 + 取消未启动的;启动一个 mem-sync-drain 守护线程 wait 已经 in-flight 的任务最多 _SYNC_DRAIN_TIMEOUT_S=5.0(第 922 行)
  2. 反序遍历 self._providersprovider.shutdown() ── 后注册的先关(external provider 大多依赖 builtin 的 snapshot,先关 external 才能保证 builtin snapshot 还在)

关键设计:

  • 5s 上限来自 docstring 第 41-44 行 ── "A wedged provider must never block process teardown indefinitely — the worker threads are daemon, so anything still running past this window dies with the interpreter."
  • cancel_futures=True 兼容处理 ── 第 904 行 except TypeError 兜底老 Python(cancel_futures 是 3.9+ 才有)

本节小结

  • MemoryManager(第 313 行)封装 fan-out / 后台化 / 路由表 / shutdown 排空
  • 单 worker ThreadPoolExecutormax_workers=1)保证 turn 顺序,daemon 线程避免阻塞 exit
  • add_provider() 强制 1 个外部 provider + 拒绝 shadow core tool
  • _strip_skill_scaffolding() 在 fan-out 前过滤 skill 展开污染
  • flush_pending() 用 sentinel barrier + timeout 避免永久阻塞

四、8 个内置 provider 的差异化定位

Layer 视角 ─ 这一层解决什么?

8 个 provider 共享同一个 ABC,差异全在存储后端(云服务 / 本地 SQLite / 知识图谱)和召回策略(dialectic Q&A / container-tag / HRR retrieval / fact extraction)。这一节不展开每个 provider 的内部实现 ── 那需要单开一篇 ── 而是给读者一张"我该选哪个"的对比表,以及每个 provider 的差异化特征

Hermes 内置 provider 的总数 8 个(bundled 在 plugins/memory/)。从 memory provider 列表(见 discover_memory_providers() 第 146 行)扫描得到,不算 user-installed。

4.1 provider 全景对照

结构:

8 个内置 provider 的差异化特征

  • honchoplugins/memory/honcho/__init__.py 第 191 行name 第 239 行)── HonchoMemoryProvider,dialectic Q&A + peer cards,AI-native user modeling,recall_mode 三选一(context / tools / hybrid)
  • mem0plugins/memory/mem0/__init__.py 第 119 行name 第 139 行)── Mem0MemoryProvider,服务端 fact extraction,LLM 抽取 user facts,rate-limit breaker
  • supermemoryplugins/memory/supermemory/__init__.py 第 440 行name 第 472 行)── SupermemoryMemoryProvider,container-tag,按 tag 隔离 context
  • byteroverplugins/memory/byterover/__init__.py 第 172 行name 第 182 行)── ByteRoverMemoryProvider,brv CLI 本地优先,基于外部 brv 进程
  • hindsightplugins/memory/hindsight/__init__.py 第 582 行name 第 668 行)── HindsightMemoryProvider,knowledge graph + 多策略 retrieval,daemon-based
  • holographicplugins/memory/holographic/__init__.py 第 115 行name 第 125 行)── HolographicMemoryProvider,SQLite + HRR,holonomic retrieval & recall
  • openvikingplugins/memory/openviking/__init__.py 第 1678 行name 第 1720 行)── OpenVikingMemoryProvider,双向 context database,HTTP client + deferred finalizers
  • retaindbplugins/memory/retaindb/__init__.py 第 452 行name 第 474 行)── RetainDBMemoryProvider,SQLite-backed write queue + 语义搜索

4.2 激活方式 ── memory.provider 配置

源码视角 ─ 配置读取(plugins/memory/__init__.py 第 339-351 行):

def _get_active_memory_provider() -> Optional[str]:"""Read the active memory provider name from config.yaml.Returns the provider name (e.g. "honcho") or None if noexternal provider is configured. Lightweight — only reads config,no plugin loading."""try:from hermes_cli.config import load_configconfig = load_config()return cfg_get(config, "memory", "provider") or Noneexcept Exception:return None

关键设计:

  • 单一 active 字段 ── memory.provider 字符串字段,值如 "honcho",缺省/空 = 仅 builtin
  • 轻量读取 ── docstring 强调 "no plugin loading" ── 这一步只读 config,避免 argparse setup 阶段就 import 所有 provider 的 SDK
  • 只有 active provider 的 CLI 暴露 ── discover_plugin_cli_commands()第 354-450 行)只对 memory.provider 指定的 provider 调 register_cli(subparser)

4.3 honcho 的差异化特性

细节:

选 honcho 当例子 ── 因为它是 8 个 provider 中配置最复杂的一个(plugins/memory/honcho/__init__.py 1240 行):

  • 3 种 recall_mode(第 204 行 self._recall_mode = "hybrid")── "context"(每 turn 自动注入)/ "tools"(仅当 model 主动调 tool)/ "hybrid"
  • 5 个 tool schemasALL_TOOL_SCHEMAS 第 184 行)── PROFILE_SCHEMA / SEARCH_SCHEMA / REASONING_SCHEMA / CONTEXT_SCHEMA / CONCLUDE_SCHEMA
  • cron guard(第 236 行 _cron_skipped)── 在 initialize() 第 291 行检查 agent_context in {"cron", "flush"},匹配则整个 provider 静默 ── 避免 cron 系统 prompt 写入污染 user representationagent/memory_provider.py 第 73-75 行 docstring 同样说明)
  • native configsave_config() 第 252-266 行)── 写 $HERMES_HOME/honcho.jsonatomic_json_write from utilsmode=0o600 权限

本节小结

  • 8 个 provider 共享同一个 ABC,差异化在存储后端 + 召回策略
  • 激活方式:memory.provider 字符串字段(plugins/memory/__init__.py 第 339 行)
  • CLI 只暴露 active provider ── discover_plugin_cli_commands() 第 354 行
  • honcho 是最复杂的 provider,cron guard 防止系统 prompt 污染 user representation

五、插件发现与 active provider 选择

Layer 视角 ─ 发现机制解决什么?

8 个 bundled provider + N 个 user-installed provider 怎么被 run_agent.py 找到?答案在 plugins/memory/__init__.py 自己的发现机制 ── 独立于通用 hermes_cli/plugins.pyPluginManager。理由:memory provider 的发现需要 "register(ctx)" 模式 + abc 兼容性检查 + relative import 处理,比通用 plugin manager 的复杂度高,单独搞一套更清晰。

5.1 双目录扫描 ── bundled 优先

源码视角 ─ _iter_provider_dirs()plugins/memory/__init__.py 第 90-121 行):

def _iter_provider_dirs() -> List[Tuple[str, Path]]:"""Yield (name, path) for all discovered provider directories.Scans bundled first, then user-installed. Bundled takes precedenceon name collisions (first-seen wins via seen set)."""seen: set = set()dirs: List[Tuple[str, Path]] = []# 1. Bundled providers (plugins/memory//)if _MEMORY_PLUGINS_DIR.is_dir():for child in sorted(_MEMORY_PLUGINS_DIR.iterdir()):if not child.is_dir() or child.name.startswith(("_", ".")):continueif not (child / "__init__.py").exists():continueseen.add(child.name)dirs.append((child.name, child))# 2. User-installed providers ($HERMES_HOME/plugins//)user_dir = _get_user_plugins_dir()if user_dir:for child in sorted(user_dir.iterdir()):if not child.is_dir() or child.name.startswith(("_", ".")):continueif child.name in seen:continue  # bundled takes precedenceif not _is_memory_provider_dir(child):continue  # skip non-memory pluginsdirs.append((child.name, child))return dirs

关键设计:

  • bundled 永远优先 ── seen set(第 115 行 if child.name in seen: continue),user 装的同名 provider 被静默跳过
  • user plugin 必须 __init__.pyregister_memory_providerMemoryProvider ── _is_memory_provider_dir() 第 74-87 行做轻量文本扫描(前 8192 字节),不真正 import
  • _get_user_plugins_dir() 第 64-71 行 ── 读 get_hermes_home() / "plugins",目录不存在返回 None,profile 隔离生效

5.2 _load_provider_from_dir ── register(ctx) + 兜底

细节:

plugins/memory/__init__.py 第 208-316 行,三阶段加载:

  1. register(ctx) 模式(第 296-303 行)── 创建 _ProviderCollector()(第 319-336 行),调 mod.register(collector),collector 截获 register_memory_provider(provider) 调用
  2. 兜底找 MemoryProvider 子类(第 306-314 行)── 遍历模块属性,issubclass(attr, MemoryProvider) and attr is not MemoryProviderattr() 实例化
  3. namespace 隔离(第 219 行)── bundled 用 plugins.memory.<name>,user-installed 用 _hermes_user_memory.<name>(第 39 行常量),避免 sys.modules 冲突

_ProviderCollector第 319-336 行)是 PluginContext 的精简替代 ── 只实现 register_memory_provider(),其他 register_tool / register_hook / register_cli_command 全部 no-op。

5.3 发现流程的可观察面

契约:

3 个公开函数(plugins/memory/__init__.py):

  • discover_memory_providers() -> List[Tuple[name, desc, available]]第 146-180 行)── 列所有 provider,含 description(从 plugin.yaml 读)和 is_available() 状态
  • load_memory_provider(name) -> Optional[MemoryProvider]第 183-205 行)── 按名加载,返回实例
  • discover_plugin_cli_commands() -> List[dict]第 354-450 行)── 只暴露 active provider 的 CLI subcommand

本节小结

  • 独立于通用 PluginManager ── plugins/memory/__init__.py 自己实现发现
  • bundled 优先 / user fallback ── _iter_provider_dirs() 第 90 行
  • user plugin 通过轻量文本扫描检测 ── _is_memory_provider_dir() 第 74 行
  • Namespace 隔离避免 sys.modules 冲突 ── _USER_NAMESPACE 第 39 行

六、prompt caching 边界 ── prefetch 的可控性

Layer 视角 ─ prefetch 这一层解决什么?

第 1 篇讲过 Hermes 的"per-conversation prompt caching is sacred" ── 长对话复用 cache 前缀,每次 API call 的 cost 跟 message diff 强相关。Memory prefetch 注入的 recall context 是每 turn 都可能变的 ── 如果注入位置/形式不对,会让 cache 频繁 invalidation。

Memory 子系统怎么处理这个边界?两个核心机制:

  • 注入位置稳定 ── prefetch 内容总是追加在 user message 尾部agent/run_agent.py,由 memory_manager.prefetch_all(user_msg) 拼接),不污染 system prompt 前缀
  • fence 标记 ── agent/memory_manager.py 第 112-120 行 三个 regex 用于清洗可能被 provider 加进 assistant_content 的 <memory-context> 内部标签 / "System note: recalled memory" 提示

6.1 context fencing ── 防止 provider 标签污染 turn 写入

细节:

agent/memory_manager.py 顶部 3 个 regex(第 112-120 行):

_FENCE_TAG_RE = re.compile(r'', re.IGNORECASE)
_INTERNAL_CONTEXT_RE = re.compile(r'<\s*memory-context\s*>[\s\S]*?',re.IGNORECASE,
)
_INTERNAL_NOTE_RE = re.compile(r'\[System note:\s*The following is recalled memory context,\s*'r'NOT new user input\.\s*Treat as (?:informational background data|'r'authoritative reference data[^\]]*)\.\]\s*',re.IGNORECASE,
)

关键设计:

  • _FENCE_TAG_RE ── 匹配单个开始/结束标签,用于清洗
  • _INTERNAL_CONTEXT_RE ── 匹配整段 <memory-context>...</memory-context>,用于从 assistant_content 剥出 provider 自己加的标记
  • _INTERNAL_NOTE_RE ── 匹配 "System note: ... NOT new user input" 这段提示文本,防止 assistant 在 turn 末尾注入的"system prompt"被记到 memory store

6.2 StreamingContextScrubber ── 流式清洗

源码视角 ─ class StreamingContextScrubberagent/memory_manager.py 第 131 行起):

对 streaming 输出的 provider(assistant 边吐字边写到 memory)做 fence 剥离 ── reset()(第 165 行)/ feed(chunk)(第 170 行)/ flush()(第 216 行)三个方法。配合 build_memory_context_block()第 296 行)构造注入的 memory block。

6.3 prefetch 注入位置的稳定性

理由:

prompt caching 的核心约束:system prompt + message 前缀稳定。Memory prefetch 必须挂在user message 尾部,而不是:

  • 插入 system prompt 中间 ── 直接破坏 cache 稳定前缀,每次 recall 都 invalidate 整个 system prompt cache
  • 作为独立 message ── 多一条 message 让 messages 列表的 alternating 模式(user/assistant/tool)变复杂
  • 追加到当前 user message 末尾 ── cache 仍然稳定,recall 内容跟着 turn 走

这个约定由 run_agent.py 在拼装 user message 时遵守 ── MemoryManager 不需要知道 cache 怎么实现的,它只负责调 prefetch_all() 拿到字符串,让上层去拼

LessonMemory 是 system prompt 的"边界外" ── Hermes 把 memory 内容追加到 user message 末尾而不是 system prompt,是 cache 友好的关键。任何"想往 system prompt 里塞动态内容"的设计都会破坏 caching,付出的成本跟内容大小成正比 ── 10k token 的 recall 内容每 turn invalidate 一次就是 10k token × 每 turn cost。

本节小结

  • prefetch 内容必须挂在 user message 尾部,不污染 system prompt 缓存前缀
  • 3 个 context fence regex(agent/memory_manager.py 第 112-120 行)清洗 provider 注入的标签
  • StreamingContextScrubber 处理流式输出场景
  • Lesson ─ memory 是 system prompt 边界外的内容,注入位置决定 caching 成本

七、FAQ 20 问

FAQ 分组说明

本节围绕 Memory 子系统,每条都是"读整个系列前最该知道的事"。从 ABC 契约、MemoryManager 编排、provider 差异、prompt caching 边界、跨 profile 隔离五个维度展开。

Q1. MemoryProvider ABC 强制实现哪些方法?

3 个 abstractmethod + 0 个 concrete 必需覆写。3 个 abstractmethod 是 name(property,第 45 行)/ is_available(第 52 行)/ initialize(第 60 行)/ get_tool_schemas(第 133 行)共 4 个;其余 11 个方法(prefetch / queue_prefetch / sync_turn / shutdown / on_*)都是默认 no-op 或 return ""。

Q2. sync_turn() 为什么不直接同步执行?

因为有 provider 会卡 298s。agent/memory_manager.py 第 528 行 docstring 记录的真实案例:Hindsight daemon 配置错误时 sync_turn 会阻塞 ~298s 才失败。如果 inline 调,run_conversation 会一直标 "running",后续用户消息触发 aggressive interrupt。所以 sync_all()(第 515 行)通过 _submit_background()(第 575 行)dispatch 到单 worker ThreadPoolExecutor

Q3. 为什么不允许多个外部 provider 同时激活?

避免 schema 膨胀 + backend 冲突。MemoryManager.add_provider() 第 343-355 行强制:第 2 个非 builtin provider 被 reject + warning。如果同时激活 honcho + mem0,两边都贡献 tool schema(get_all_tool_schemas() 第 645 行),agent 一次对话可能要选 5+ 个 recall tool,且两边对同一 turn 的写入可能互相覆盖 ── 用户通过 memory.provider 配置字段二选一

Q4. _has_external 标志在 __init__ 怎么初始化?

False。agent/memory_manager.py 第 323 行 self._has_external: bool = False。第 1 个非 builtin provider 加入时翻成 True(第 356 行),后续任何非 builtin 都会被 reject。

Q5. _sync_executor 什么时候创建?

首次 sync_all() / queue_prefetch_all() 时 lazy 创建。agent/memory_manager.py 第 604 行 _get_sync_executor() 用双检锁 lazy 化,纯 builtin 路径永远不起线程。

Q6. ThreadPoolExecutor 为什么是 max_workers=1

保证 turn 顺序。agent/memory_manager.py 第 612 行 max_workers=1,单 worker 串行化所有 sync/prefetch 任务。turn N 必然先于 turn N+1 落盘 ── sync_all() 第 536-538 行 docstring 明说 "provider implementations don't need their own ordering guarantees"。

Q7. _strip_skill_scaffolding() 在哪个时机调用?

prefetch_all() / sync_all() / queue_prefetch_all() 三个入口都先过。agent/memory_manager.py 第 458 行(prefetch)/ 第 544 行(sync)/ 第 485 行(queue)三处。一次过滤、整个 fan-out 共享,避免每个 provider 各自实现。

Q8. on_session_switch()resetrewound 区别?

reset=True 是新会话,rewound=True 是同会话 transcript 截断。agent/memory_provider.py 第 204-214 行 docstring:reset=True 触发 /reset / /new,provider 应清空 per-session buffer;rewound=True 触发 /undo,只 invalidate per-turn document state。MemoryManager.on_session_switch() 第 761-762 行只 forward rewound=True(不 forward False)── 避免污染 provider 的 **kwargs

Q9. on_delegation() 为什么只在 parent 调?

因为 subagent 不持 provider。agent/memory_provider.py 第 234-237 行 docstring:subagent 通过 skip_memory=True 创建(run_agent.py),没有自己的 MemoryManager。parent 收到 task + result pair 后 mirror 到自己的 provider,方便 recall "上次我委派的那个任务"。

Q10. discover_memory_providers() 什么时候被调?

CLI hermes memory list 这种 list-style 子命令。实际加载走 load_memory_provider(name)(第 183 行),由 run_agent.pyMemoryManager.add_provider() 之前调一次。CLI 暴露走 discover_plugin_cli_commands()(第 354 行),只暴露 active provider 的 subcommand

Q11. bundled provider 和 user-installed provider 冲突怎么办?

bundled 永远优先。plugins/memory/__init__.py 第 90-121 行 _iter_provider_dirs()seen set 标记 bundled,user 装的同名 provider 被静默跳过。如果用户想替换 bundled provider,只能改 bundled 源码(PR 流程)或 fork。

Q12. _USER_NAMESPACE 为什么是独立 namespace?

避免 sys.modules 冲突。plugins/memory/__init__.py 第 39 行常量 + 第 217-219 行使用:bundled provider import 为 plugins.memory.<name>,user-installed import 为 _hermes_user_memory.<name>。如果用户装的 _hermes_user_memory.honcho 和 bundled 的 plugins.memory.honcho 用同一个 name,两者完全隔离

Q13. on_memory_write() 怎么触发?

builtin memory tool 写入时。Hermes 内置 memory tool(toolset memory)写 entry 时调 MemoryManager.on_memory_write()agent/memory_manager.py 第 822 行)→ 跳过 builtin 自身(第 834 行 if provider.name == "builtin": continue)→ fan-out 到其他 provider。

Q14. on_memory_write() 的 metadata_mode 怎么处理?

三种 mode:keyword / positional / legacy。agent/memory_manager.py 第 796-820 行 _provider_memory_write_metadata_mode()inspect.signature() 检查 provider on_memory_write 签名:接受 **kwargsmetadata= → keyword;接受 4 个 positional → positional;其他 → legacy(不传 metadata)。

Q15. flush_pending() 用什么技巧实现 barrier?

单 worker 的 sentinel future。agent/memory_manager.py 第 620-641 行:executor.submit(lambda: None) 排队一个 sentinel,由于 max_workers=1,sentinel 之前的所有 task 必然都已完成fut.result(timeout=...) 等 sentinel 执行完即 barrier complete。

Q16. _SYNC_DRAIN_TIMEOUT_S=5.0 是怎么定的?

经验值。agent/memory_manager.py 第 45 行常量,docstring 第 41-44 行:"A wedged provider must never block process teardown indefinitely"。线程本身是 daemon,5s 内没跑完的 sync 会在 interpreter 退出时被 kill

Q17. shutdown_all() 为什么反序遍历 providers?

后注册的先关。agent/memory_manager.py 第 876 行 for provider in reversed(self._providers)。理由:external provider(后注册)大多依赖 builtin 的 snapshot / table,先关 external 才能保证 builtin 资源关时仍然可用

Q18. builtin provider 在哪里实现?

agent/builtin_memory_provider.py(Hermes 内置)。本篇不展开。builtin provider 负责 ~/.hermes/memory.json + user.json 的本地文件存储(具体路径在 builtin provider 实现里),永远存在add_provider() 第 341 行 builtin 不算 external)。

Q19. 跨 profile 隔离怎么实现?

agent_identity 作为 provider 端 namespace。agent/memory_provider.py 第 76-77 行 docstring:initialize(**kwargs)agent_identity 是 profile name(如 "coder"),provider 用它做 per-profile namespace。同时 get_hermes_home() 已经是 profile-scoped(HERMES_HOME 已被 _apply_profile_override() 改),两条隔离路径叠加

Q20. 新 provider 怎么接入?

bundled 改 plugins/memory/<name>/,user 装到 $HERMES_HOME/plugins/<name>/两种路径都要求 __init__.pyregister(ctx) 或一个 MemoryProvider 子类。plugin.yaml 写 description 用于 list 命令。save_config()get_config_schema() 二选一(agent/memory_provider.py 第 273-277 行强制)。

FAQ 全篇总纲

  • 契约:3 abstract + 11 concrete(Q1 / Q10 / Q20)
  • 后台化:单 worker + sentinel barrier + 5s drain(Q2 / Q6 / Q15 / Q16)
  • 单 provider_has_external 标志 + core tool shadow 拒绝(Q3 / Q4)
  • 生命周期reset / rewound / delegation 语义(Q8 / Q9 / Q17)
  • 发现:bundled 优先 + namespace 隔离 + 轻量文本扫描(Q11 / Q12 / Q19)

下一篇预告

第 9 篇 ─ Gateway 多平台适配(Telegram / Discord / Slack / 飞书 / 钉钉 / 企业微信 / 邮件 …)(敬请期待)

Memory 子系统覆盖完了 ── 模型怎么调工具(Layer 2)、怎么执行命令(Layer 2.5)、怎么记长期事实(Layer 3)。下一篇会展开 Layer 4:

  • BaseAdapter 怎么定义 send_message / edit_message / on_update 契约
  • GatewayRunner ── 20+ 平台 adapter 的 orchestrator
  • 每个平台 adapter 的差异 ── 跟 Layer 2.5 / Layer 3 同样的窄腰模式(BaseAdapter ↔ MessageBus
  • 消息队列 + 反压 + session lifecycle
  • Slash 命令跨平台路由
  • plugin platform adapter 的注册路径(PluginContext.register_platform()

Layer 2 / Layer 2.5 / Layer 3 / Layer 4 四层结构完整后,下一步是 Layer 5 ─ Cron 调度 + 跨进程边界。


http://www.jsqmd.com/news/1209535/

相关文章:

  • Matlab MIMO-OFDM点对点通信仿真包:含8阵元ULA波束赋形、导频信道估计与BER性能可视化
  • 别卷 Agent 编排了:2026 年后端求职的“脏活”才是 Offer 分水岭
  • Vivado 2018.2入门工程实操包:带完整构建日志、运行记录与可直接加载的设计文件
  • 2026年7月最新南宁天梭官方售后热线及客户服务网点地址 - 天梭服务中心
  • 51单片机串口通信仿真包:Proteus电路+Keil源码+可烧录hex文件
  • 真实道路场景下的蓝牌黄牌新能源绿牌检测识别训练图集
  • MATLAB双层调度代码包:兼顾风电不确定性的电动汽车时空协同优化方案
  • chaosArsenal-OS社区贡献指南:从用户到开发者的完整路径
  • 电商企业如何选适配的AI数字人直播厂商?2026年深度评测给出清晰选型路径
  • 医院门诊药房后台管理系统(Java Web实现,含源码、数据库与论文)
  • 柔软亲肤湿厕纸哪家好:联盛森宝温润亲肤 - MXyuyu
  • 重塑游戏体验:Wand-Enhancer如何为游戏修改工具注入新活力
  • 2026超载超限汽车检测设备品牌前十,浙江润鑫精准治超获好评,品质靠谱 - 品牌速递
  • C++课程设计实战:基于QT与SQLite的教室管理系统开发指南
  • 雷达中国官方售后服务中心|官方地址及售后服务电话权威信息声明(2026年7月更新) - 亨得利钟表维修中心
  • 浪琴中国官方售后服务中心|服务热线及全部维修详细地址权威信息通告(2026年7月最新) - 浪琴官方售后服务中心
  • 2026指南:老房局部翻新专业服务公司甄选 - 甄选服务推荐
  • C/C++笔试核心:从指针内存到虚函数表的立体知识体系构建
  • Codex Skills 入门指南:五大核心工程决策模块解析
  • 消毒产品生产许可证(卫生许可证)办理流程全解
  • AI Agent稳定性核心:Chrome调试协议实战避坑指南
  • Cursor 3.1:代码理解力驱动的编辑器革命
  • 积家官方售后服务中心全部地址与客服热线实地考察报告+多信源验证(2026年7月更新) - 积家官方售后服务中心
  • 2026年07月消防球墨铸铁管供应厂家实力与选型框架解析 - 甄选服务推荐
  • 2026年最新教程:微信里有什么拼豆小程序好用推荐 - 省事研究所
  • STC89C52红绿灯实战工程:带倒计时、黄灯过渡、紧急模式的可烧录完整项目
  • 广州做小程序商城的公司有哪些,2026从这五个维度筛选不踩坑
  • 冲绳科学技术大学院大学研究者们找到了让AI视觉学习更聪明的秘密
  • 浪琴中国官方售后服务中心|全新地址及24小时服务电话权威信息通知(2026年7月最新) - 浪琴服务中心
  • STM32F10x芯片程序加密实操包:RDP等级设置+FLASH保护+Keil工程一键导入