研究日期:2026-07-23(Asia/Shanghai)
主仓库:NousResearch/hermes-agent
源码快照:3f9944bad92ed00f9116cfbad6326cceecb39151
快照提交时间:2026-07-22T23:36:45-04:00
包版本:0.19.0
最新正式版本:v2026.7.20,Hermes Agent v0.19.0(Quicksilver,2026-07-20)
版本声明
本文研究的是 2026-07-23 抓取的 main 快照,它比最新正式版本标签晚约三天。仓库变化极快,所以正文不把 main 中的实现自动描述为所有稳定安装都已具备;关键源码链接全部钉到完整 SHA。官方 v0.19.0 release 宣称首轮 time-to-first-token 从约 4.3 秒降到约 0.9 秒,这是项目方的发布数据,不当作独立基准测试。
结论先行
Hermes Agent 的长期记忆不是一个统一的数据库,而是四种不同可见时机:
MEMORY.md/USER.md在 system prompt 构建时形成冻结快照;- 外部 provider 的静态说明也只在 system prompt 构建时进入稳定前缀;
- 与本轮问题相关的 recall 在第一条模型请求前取得,追加到当前 user message 的
api_content旁路; - 完整轮次结束后,外部记忆写回和下一轮预热进入单 worker FIFO。
最有价值的设计不是“缓存记忆”,而是 persist what you send:Hermes 把当时真正发给模型的 user message 字节与干净 transcript 一起保存。下一轮、甚至进程重启后,历史消息会重放同一份 api_content,因此外部 recall 可以每轮变化,已经发生过的 prompt prefix 仍然按字节稳定。
这是一种时序架构:记录、召回、采用为 system-level identity、持久化和再次可见,被拆成了不同事件。
产品层、架构层、代码层
产品层
- Hermes 是跨 CLI、TUI、desktop、gateway、ACP 和 API 的通用 agent,而不是单独的 memory SDK。
- 内置文件记忆强调小、可读、可编辑;SQLite session history 负责会话连续性与检索;Honcho、Mem0、Hindsight、OpenViking 等外部 provider 提供自动召回和写回。
- 同时最多启用一个外部 memory provider,但内置文件记忆可以并存。
架构层
- Gateway 负责授权、路由、session ownership、并发 lease、历史加载、agent cache 和结果落库。
AIAgent是各入口共享的窄腰,拥有稳定 system prompt、model/tool loop、memory manager 和 SessionDB。TurnContext是记忆时序的关键边界:它在第一条 LLM 请求前执行 provider recall,组成api_content,再把 inbound user turn 持久化。MemoryManager统一 provider 注册、静态 prompt、当前轮 prefetch、完成轮 sync、下一轮 warmup、session boundary 和 bounded shutdown。SessionDB用 SQLite + WAL + FTS5 保存 transcript;content留下干净语义,api_content保存真实 wire bytes。
代码层
gateway/run.py::_handle_message
→ SessionStore.get_or_create_session()
→ acquire turn lease by resolved session_id
→ load_transcript()
→ _run_agent()
→ reuse/create AIAgent
→ AIAgent.run_conversation()
→ agent/turn_context.py::build_turn_context
→ MemoryManager.on_turn_start()
→ MemoryManager.prefetch_all()
→ compose_user_api_content()
→ persist inbound user row
→ agent/conversation_loop.py
→ replay historical api_content
→ model call
→ persist assistant tool_calls before side effects
→ model_tools.handle_function_call()
→ append tool result
→ repeat until final response
→ turn_finalizer.finalize_turn()
→ sync_all()
→ queue_prefetch_all()
→ gateway persists/delivers without duplicating agent-owned rows
一条真实请求链
以下以 gateway 中的一条消息为例:用户先说“以后这个项目的 staging PostgreSQL 端口是 55432,请记住”,下一轮再问“帮我检查 staging 数据库”。
1. Gateway 先占住 session
GatewayRunner._handle_message() 做授权和命令处理后,在任何 await 之前把 _AGENT_PENDING_SENTINEL 放入 _running_agents。否则语音、图片分析或 session hygiene 期间到来的第二条消息可能为同一 session 启动第二个 agent,交错写 transcript。
session 解析完成后,Gateway 再按最终 session_id 获取 turn lease,把“加载历史 → 执行 → flush”串行化。它解决的是两个不同 routing key 指向同一个 session 的别名竞态。
证据:
gateway/run.py:10284-10295gateway/run.py:11747-11769gateway/run.py:12374-12429gateway/run.py:12628-12654
2. system prompt 不是每轮重新拼
AIAgent 初始化时建立 MemoryStore,从 MEMORY.md / USER.md 读取 live entries,并另存一份 _system_prompt_snapshot。build_system_prompt() 把身份、工具规则、项目上下文、文件记忆、外部 provider 静态块和日期拼起来;结果缓存于 agent,只在新 session 或 compression 触发 invalidation 后重建。
因此本轮调用 memory tool 把端口写进 MEMORY.md,磁盘与 live state 会立刻改变,但当前 system prompt 不会中途变字节。压缩重建或新 session 才采用新的文件快照。
证据:
agent/agent_init.py:1572-1600tools/memory_tool.py:123-146tools/memory_tool.py:178-215agent/system_prompt.py:480-550agent/system_prompt.py:553-561
3. 外部 recall 走 user-message 旁路
在当前轮进入 tool loop 前,build_turn_context() 先调用 on_turn_start(),随后只做一次 prefetch_all(query)。外部 provider 的调用放进 daemon thread,但当前轮最多会等待 8 秒;超时则本轮无 recall,且在卡住的调用返回前不再为同一 provider 并发启动第二个 prefetch。
召回结果不会改 system prompt,而是被包进 <memory-context>,与 plugin context 一起追加到当前 user message 的 API 副本:
# 简化伪代码,不是原仓库逐字摘录
recall = memory_manager.prefetch_all(clean_user_text)
wire_content = compose_user_api_content(clean_user_text, recall, plugin_context)
message["content"] = clean_user_text
message["api_content"] = wire_content
session_db.append_message(message) # 第一条模型请求前
第一次模型请求看到的于是是:
帮我检查 staging 数据库
<memory-context>
这个项目的 staging PostgreSQL 端口是 55432
</memory-context>
而 transcript 的 content 仍是用户原话。
证据:
agent/memory_manager.py:515-585agent/turn_context.py:49-81agent/turn_context.py:939-1015agent/turn_context.py:1012-1028
4. “发了什么,就重放什么”
Hermes 不能只保存干净 user text。若下一轮历史里缺少上轮 recall,provider 看到的 prefix 会从上轮 user message 处开始分叉,后面的 assistant/tool chain 都需要重新 prefill。
所以 api_content 与消息一起写入 SQLite。构造 API payload 时:
- 当前轮使用刚组成的
api_content; - 历史 user / assistant message 若有 sidecar,就用它替换
content; - sidecar 自身不会发送给 provider;
- 若 compaction 或清理逻辑重写了 message content,Hermes 会丢弃旧 sidecar,宁可发生一次 cache miss,也不重放已被删除的内容。
测试用本地 mock provider 验证了两个不变量:同一轮的多次 model call 使用相同 user bytes;全新 AIAgent 从 SessionDB 恢复后,turn N+1 重放的 turn N user message 与 turn N 当时发送的 JSON 字节相等。
证据:
agent/turn_context.py:84-116agent/conversation_loop.py:973-1021agent/conversation_loop.py:1046-1065hermes_state.py:5454-5581tests/agent/test_api_content_sidecar.py
5. tool call 先落库,再执行副作用
模型返回 tool calls 后,conversation loop 先把 assistant tool-call message 追加并持久化,再分派工具。这样进程若在副作用期间退出,恢复逻辑至少知道模型已经请求过什么,不会只留下孤立的 tool result。
安全 tool batch 可以并行,但调度器把 sequential call 当作 barrier;tool result 再回灌 message list,循环继续到 final response 或 iteration budget。
证据:
agent/conversation_loop.py:5267-5317run_agent.py:6396-6436model_tools.py:1055-1377
6. 只有完成的轮次才进入外部长期记忆
turn finalizer 在最终响应完成后调用 _sync_external_memory_for_turn()。若 interrupted=True、没有最终响应或没有 user-origin message,就跳过写回;半截回复、被打断的 tool chain 不应成为持久事实。
正常轮次的 sync_all() 和 queue_prefetch_all() 不在线程内阻塞用户响应,而是排入单 worker:
sync turn N
→ warm next-turn recall for turn N
→ sync turn N+1
→ warm next-turn recall for turn N+1
FIFO 保证轮次顺序。真正 session 结束时才执行 on_session_end() 和 shutdown,不会在每次 run_conversation() 后杀死 provider。
证据:
run_agent.py:3661-3720agent/turn_finalizer.py:583-608agent/memory_manager.py:628-684agent/memory_manager.py:688-747agent/memory_manager.py:855-914
7. 持久化是 best-effort,但不是静默丢弃
关机先给后台 FIFO 最多 5 秒排空。如果 provider 永久卡住,Hermes 不让进程无限等待;仍未开始的 write / prefetch 会被取消并计数,已经运行的 daemon task 与进程一起结束,日志记录 abandoned_writes、abandoned_prefetches 和 detached active tasks。
这是明确取舍:对话可用性优先于第三方记忆后端的绝对写入保证。需要严格 durability 的场景必须在 provider 内增加 outbox、幂等键或独立 worker,不能把 sync_turn() 当事务提交。
证据:
agent/memory_manager.py:42-47agent/memory_manager.py:1134-1212tests/agent/test_memory_async_sync.py
记忆时序矩阵
| 记忆路径 | 写入 / 读取时机 | 本轮可见性 | 下轮 / 重启可见性 | 关键代价 |
|---|---|---|---|---|
| MEMORY.md / USER.md live state | agent init 读取;tool call 可写 | 工具结果可见;已缓存 system prompt 不变 | 新 session 或 compression rebuild 进入 prompt | 当前行为会暂时使用旧快照 |
| 内置文件快照 | system prompt 构建 | 全轮稳定 | 随 session prompt 恢复或重建 | 有字符上限,只适合精选事实 |
| 外部 provider 静态块 | system prompt 构建 | 全轮稳定 | 新 agent / rebuild 更新 | 不能承载每轮动态 recall |
| 外部 prefetch() | 本轮第一条模型请求前,最多等 8 秒 | 通过 api_content 立即可见 | sidecar 原样重放 | 超时会跳过;卡住时不并发重试 |
| sync_turn() | 完整、未中断轮次结束后 | 不改变本轮 | provider 完成写入后可召回 | 后台 best-effort,进程异常可丢 |
| queue_prefetch() | 完整轮次结束后 | 不可见 | 为下一轮预热 | 与写回共享单 worker,慢写会拖后预热 |
| SessionDB transcript | user turn 在第一条 LLM call 前;assistant/tool 随循环写 | 支撑 crash recovery | SQLite + WAL + FTS5 恢复 / 搜索 | 多进程写争用需重试与维护 |
| api_content sidecar | 与 message 一起保存 | 当前 wire bytes | 历史按字节重放 | 存储量增加;内容重写时必须失效 |
五个设计判断与取舍
1. system prompt 冻结,动态 recall 下沉到 user message
- 收益:provider prefix cache 可复用,memory backend 仍能每轮更新召回。
- 成本:外部 context 的指令优先级低于 system;内置文件新写入不会立刻成为 prompt identity。
- 失败方式:若把动态 context 又塞回 system prompt,会重现 issue #13631 描述的 cache invalidation。
- 适用:长 session、多次 tool round、prefix caching 显著的模型。
2. content 与 api_content 分开保存
- 收益:人类看到干净 transcript,模型得到可重放的历史 wire bytes。
- 成本:每个 rewrite / compaction / export 路径都必须理解 sidecar,维护面扩大。
- 失败方式:sidecar 与真实发送内容漂移会造成错误重放;Hermes 选择在不确定时删除 sidecar。
3. 当前 recall 有超时,完成轮写回异步化
- 收益:需要 recall 的当前问题可以使用结果;第三方写入不会让界面长期保持“agent running”。
- 成本:首轮仍可能为 recall 多等最多 8 秒;后台写入不是事务 durability。
- 失败方式:provider 卡死时当前轮跳过 recall,关机期限后可能放弃 queued write。
4. 一个外部 provider,而不是任意组合
- 收益:避免 tool schema 膨胀、生命周期冲突和两个后端争夺同一记忆所有权。
- 成本:不能直接把多个专长后端串成 ensemble。
- 适用:个人助手和单一治理边界;复杂企业 memory mesh 需要在 provider 内部聚合。
5. session history 服从插入顺序
- 收益:按 SQLite autoincrement
id恢复,避免 NTP 回拨、休眠恢复导致 timestamp 倒序,把 assistant tool call 排到 tool result 后面。 - 成本:跨库导入与分支必须维护稳定插入语义。
- 失败方式:用 wall-clock 排序可能破坏 tool-call/result 邻接并触发 provider HTTP 400。
证据:
hermes_state.py:6161-6216
现场 issue 与 release 证据
Issue #13631:动态 Honcho context 破坏 prefix cache
- 状态:closed / completed,2026-06-08 关闭。
- issue 描述旧架构把不断变化的 Honcho block 放进 system prompt,导致每隔若干轮重建 agent 和 cache prefix。
- 当前源码把静态 provider block 留在 system prompt,把本轮 recall 放入 user-message
api_content,并通过 sidecar 重放历史字节。 - 文章只说“当前实现解决了 issue 所描述的架构问题”,不声称所有 provider / API 模式都有同等 cache 命中率。MoA 与
codex_app_server明确绕过 sidecar stamping。
Issue #4889:skill 展开污染 memory query
- 状态:closed / completed,2026-04-04 关闭。
- slash skill 会把完整 skill body 展开到 model-facing message。若直接拿它做 memory search / embedding,会把指令脚手架当用户意图。
- 当前
MemoryManager._strip_skill_scaffolding()在 provider fan-out 前统一提取真实 user instruction;focused tests 覆盖 prefetch、warmup 和 sync。
Issue #17251:compression 后 memory 降级
- 状态:closed / not planned,2026-06-10 关闭。
- 它提供的是历史故障语境,不足以证明当前快照仍存在同一 bug。当前 source 在 compression invalidation 时重载文件记忆,并在 message content 被重写时删除旧
api_content。 - 正文用它说明 compaction 与 memory precedence 的长期张力,不把 closed/not planned 等同于“已修复”。
Issue #17154:第三方架构审计
- 状态:closed / not planned,2026-07-12 关闭。
- 这是一份外部审计式 issue,可作为 gateway 体积、restart continuity 和 policy coverage 的问题线索,但不是官方安全结论,也不是本文主要证据。
v0.19.0 Quicksilver
- 发布时间:2026-07-20。
- 项目方宣称第一轮 TTFT 约下降 80%,并强调 desktop/TUI 性能、durable delivery ledger、secret manager、smart approvals 等。
- 本文只把 release 用作版本与产品演进背景;记忆时序判断来自当前源码和测试。
对旧 HTML / v0.1 稿的校准
- “会话开始后记忆冻结”只适用于已进入 cached system prompt 的文件快照和 provider 静态块,不适用于全部 memory。
- 外部 recall 不是“下一 session 才可见”:当前轮第一条模型请求就能通过
api_content看见。 - 外部 write 也不是“结束后一定落盘”:它是完成轮后的后台 best-effort 工作,关机排空有 5 秒上限。
- 官方 user guide 的“injects provider context into the system prompt”是概括性说法;ABC 与当前 source 明确把 static block 和 per-turn prefetched recall 分开。
- “prefetch 完全不阻塞”也过强:下一轮 warmup 是后台任务,但当前轮
prefetch_all()会等待 provider,默认最多 8 秒。 - 当前仓库约 219,040 stars(2026-07-23 GitHub API 快照),旧 HTML 的 218.8k 应更新。
- 原稿引用的
cbc1054...已过时,最终稿统一钉到3f9944...。
代表性测试与本轮验证
本轮执行:
uv run --with pytest pytest -q \
tests/agent/test_api_content_sidecar.py \
tests/agent/test_memory_async_sync.py \
tests/run_agent/test_memory_sync_interrupted.py \
tests/agent/test_memory_skill_scaffolding.py
结果:75 passed in 170.19s。
覆盖的关键行为:
- sidecar 在 SessionDB 中原样 round-trip;
- 当前轮多次 model call 使用相同 wire bytes;
- 新 agent / 新进程恢复后,历史消息重放相同 bytes;
- 慢 provider 不阻塞 turn completion;
- 单 worker 保证完成轮写回顺序;
- bounded shutdown 会显式统计被放弃的 write;
- interrupted turn 不进入外部长期记忆;
- skill body 不污染 provider query。
局限与不适用边界
- 写回不是事务 outbox。 对医疗、财务、合规审计等要求“响应成功即记忆必达”的系统,需要额外 durability 层。
- 文件快照存在有意的陈旧窗口。 一次 memory tool write 不会立刻改变当前 system prompt;这是 cache 与一致性的交换。
- 动态 recall 的优先级更低。 它位于 user message 旁路,不能替代 system policy。
- 第三方 provider 扩大隐私边界。
messages可能包含 tool call、文件路径和命令输出;provider 必须说明哪些内容离开本机。 - 单 provider 简化了所有权,也限制组合。
- 核心控制面仍很大。
gateway/run.py、run_agent.py、hermes_state.py都是大型多职责文件,读懂一次请求需要跨越较长链路。 - 不是每个 API mode 都有同样 sidecar 行为。
codex_app_server和 MoA 有明确例外。 - 缓存稳定不等于缓存命中。 上游 provider 是否缓存、key 如何定义、model 或 tool schema 是否变化,仍由更多条件决定。
同类边界比较
| 方案 | 强项 | 与 Hermes 当前设计的差别 |
|---|---|---|
| 只用向量库做 top-k RAG | 实现简单、召回灵活 | 常忽略 prompt prefix、写回时机、interrupted turn 和历史 bytes 重放 |
| 只把 profile 塞进 system prompt | 优先级高、行为直观 | 每次 profile 变化都可能让前缀失效;不适合高频动态 recall |
| OpenClaw workspace memory | 更强调 workspace 文件、Gateway 控制面和长期委托 | Hermes 在 external provider 生命周期与 api_content replay 上更显式 |
| 独立 memory SDK(如 Mem0) | 专注抽取、搜索和 storage backend | Hermes 不是替代 memory SDK,而是规定它进入 agent turn 的时序和失败语义 |
代码阅读索引
gateway/run.py:10284-10295— gateway 主链说明。gateway/run.py:11747-11769— pending sentinel。gateway/run.py:12374-12654— session、lease、history。gateway/run.py:20587-20847— agent cache / construction。gateway/run.py:21065-21414— history 恢复与run_conversation()。agent/agent_init.py:1572-1667— built-in / external memory 初始化。tools/memory_tool.py:123-215— live state 与 frozen snapshot。agent/system_prompt.py:480-561— prompt memory 与 rebuild。agent/memory_provider.py:43-174— provider 生命周期契约。agent/memory_manager.py:354-684— single-select、prefetch、sync。agent/memory_manager.py:1134-1212— bounded shutdown。agent/turn_context.py:49-126—api_contentcomposition / replay helpers。agent/turn_context.py:939-1028— current-turn memory timing。agent/conversation_loop.py:973-1065— API payload 与 stable system prefix。agent/conversation_loop.py:5267-5317— tool-call persistence / execution。run_agent.py:3661-3720— completed-turn memory commit gate。agent/turn_finalizer.py:583-608— per-turn finalization,不等于 session end。hermes_state.py:1381-1484— SQLite、WAL、并发。hermes_state.py:5454-5581— message +api_content写入。hermes_state.py:6161-6216— insertion-order replay。
主要来源
- Hermes Agent 当前源码快照
- 官方 Architecture
- 官方 Prompt Assembly
- 官方 Memory Providers 用户文档
- 官方 MemoryProvider 插件文档
- v0.19.0 Quicksilver release
- Issue #13631:Honcho context 与 prefix cache
- Issue #4889:skill scaffolding 污染 memory query
- Issue #17251:compaction 后 memory precedence
- Issue #17154:第三方架构审计
写作时必须回答的三个问题
- 如果一条记忆刚被写下,它什么时候才有资格影响 agent 的高优先级身份判断?
- 为了 prompt cache 而保存“当时真正发出的字节”,会不会让 transcript 变成两份并行真相?
- 当记忆写回是 best-effort 时,哪些产品可以接受,哪些行业必须再加一层 durable outbox?