它说任务建好了,可那个任务从来没存在过


你让 agent 建一个每天早上的定时任务。它回你一句「已创建,明天 8 点开始跑」。第二天 8 点,什么也没发生。你去看 jobs.json,里面没有这条任务;再看执行记录,也是空的;日志干净得像什么都没发生过。它没有骗你——它确实调用了那个工具,只不过结果还没落地,上下文压缩就把整轮对话换成了摘要,于是「调用过」这件事和「任务存在过」这件事,一起消失了。

这类事故的可怕之处不在于丢了一次操作,而在于没有任何人知道丢了:agent 认为它没做过,你也以为它没做过,于是没人去补。社区插件 agent-ledger(本周进入官方插件目录)就是专门盯这个缝隙的,它同时暴露了另一个更普遍的问题——你的 agent 到底做过哪些「有副作用」的事。

什么是「幽灵工具调用」

先说清楚这里的技术背景。Hermes 在上下文接近阈值时会做压缩:把较早的轮次换成一页摘要,好腾出空间继续跑。这个机制本身是必要的,但它有个边界情况:如果压缩正好发生在工具已经发出、结果还没确认落地的中间态,那一轮就被整轮替换掉了。结果就是:

  • 操作没有完成;
  • 没有报错、没有重试、没有日志;
  • agent 的会话状态里显示「从来没调过这个工具」。

上游 issue #90985 记录了一个真实案例:某次 cronjob_create 在 20:39:07 发出,任务在 jobs.json、executions.db 和 cron/output/ 里都不存在——零痕迹。issue 里给出的合理触发路径是:一轮里连发多个工具调用 → 这些输出把上下文推过压缩阈值 → 下一轮预检时压缩触发 → 历史被改写 → 调用结果随摘要一起蒸发。

在官方修好这个中间态之前(甚至之后),一个「知道自己可能丢了什么」的日志都是划算的。这就是 agent-ledger 的思路。

它怎么发现的:一张写前日志

插件挂上四个钩子,把每个会改变状态的工具调用记进一张本地日志:

时机 动作
pre_tool_call 写一条 PENDING 记录(工具名、参数、时间、session)
post_tool_call 把这条改成 CONFIRMED,或记下 ERROR:类型
pre_llm_call 对账:超过宽限期的 PENDING = 幽灵
on_session_start 会话开始时的账目对齐

只记录会改变状态的工具:write_file、patch、terminal、cronjob、delegate_task、看板相关的写入,以及所有 MCP 工具(按 mcp__ 前缀识别)。只读工具不进日志,所以日常开销是零。

宽限期不是一刀切:默认 2 分钟,但按工具区分——delegate_task 给 30 分钟、terminal 给 15 分钟,避免长任务被误判。如果一条被判为幽灵的调用后来又确认了,记录会被「复活」,而不是多出一行。插入是 WAL 模式的 SQLite 写入,官方说法是 10 毫秒以内,日志本身不拦截、不修改参数,它只是个飞行记录仪。

想自己看一眼,一条命令:

sqlite3 ~/.hermes/plugin-data/agent-ledger/ledger.db "
  SELECT ts, tool_name, status, substr(args_json,1,60)
  FROM ledger ORDER BY id DESC LIMIT 20;"

状态有四种:PENDING、CONFIRMED、ERROR:<类型>,以及 GHOST_REPORTED(已经提醒过 agent 的幽灵)。

为什么是「下一轮告诉它」,而不是当场拦住

这大概是这个插件最值得学的设计决定。

幽灵被发现时,那一轮已经结束了——你没法回头补。所以插件选择在下一轮把这件事塞回对话:pre_llm_call 钩子可以返回一段 context,它会被注入到当前这轮的用户消息里,而不是系统提示里。系统提示是缓存敏感区,改它会让缓存失效;放进用户消息,agent 就能在「它还来得及补救」的那轮对话里知道:有一个调用发出过、从未确认,先核实状态再相信它。

另一个决定同样克制:为什么不在 pre_tool_call 里直接拦住可疑调用?因为 Hermes 对 pre_tool_call 钩子是「超时即失败关闭」的——钩子跑超过 30 秒就会挡住正常的工具调用。一个慢日志不该有这种权力。日志就是日志,拦截是另一个可选功能(下面会讲)。

用起来

从官方插件目录装(本周刚进目录):

hermes plugins install agent-ledger
hermes plugins enable agent-ledger

如果你想直接用维护者的仓库版本,README 里的做法是克隆到 ~/.hermes/plugins/agent-ledger/ 再启用,启用后重启 gateway,插件会在下一个会话里加载。

装完先跑一遍自检,确认钩子签名跟你的版本对得上:

python3 selftest.py

它会验证「发出 → 确认」的配对、按工具名回退确认、幽灵检出、上下文注入、以及只记录写入类工具这几件事。

顺手多出来的:OWASP 姿态打分

插件里还有一个脚本,把日志里的活动按《OWASP Top 10 for Agentic Applications 2026》对一遍,给出 0–100 的姿态分:

python3 posture.py                        # 对当前日志打分
python3 -m posture ~/.hermes/plugin-data/agent-ledger/ledger.db

从日志元数据能覆盖到的条目是:工具误用(ASI02)、意外代码执行(ASI05)、级联失败(ASI08)、失控 agent(ASI10),外加一条社区扩展的「过度代理权」(标为 ASI11,官方清单到 ASI10 为止,作者在每处都注明了这是扩展)。评分是可复核的:分数 = 100 − 各类发现的严重度权重之和,权重为 CRITICAL 25 / HIGH 15 / MEDIUM 8 / LOW 3,发现不去重,完整清单一定一起返回,你可以自己重算一遍。

它的信任模型写在明面上,也是最该记住的一句:这本账是被打分的 agent 自己写的。一个不记录的 agent 无法被观察者打分,所以 100 分的意思是「已记录的活动里没有可疑项」,而不是「这个 agent 行为端正」。当成卫生指标看,别当成证明。

策略拦截:先跑几天 dry_run

观察是核心的事,拦截是这个插件可选加的一层。装上策略文件,先看几天「本该被拦下什么」:

cp policy.example.json ~/.hermes/plugin-data/agent-ledger/policy.json
echo dry_run > ~/.hermes/plugin-data/agent-ledger/policy_mode   # 只记录,不拦

确认这份报告里的东西你真的想拦,再切换到 enforce:命中的调用会在执行前被拒,并把原因回给 agent。

作者对覆盖面的坦诚值得原文照抄给读者:策略匹配是基于命令文本的签名匹配,像安全带,不是墙。已知的绕过方式至少有三种——用 sh -c 拼字符串(要炸的那段字面量从没出现在命令里)、用环境变量间接引用(rm -rf $TARGET)、以及把一次危险操作拆成两次各自无害的调用。它能稳定挡住的,是那些手滑级的灾难(递归删除、管道进 shell、读凭证文件),而这正好就是 dry_run 报告让你看到的东西。

数据在哪、有多大、要小心什么

  • 账本是明文本地 SQLite:<hermes_home>/plugin-data/agent-ledger/ledger.db,会记录工具参数(最多 4000 字符,包含终端命令、文件内容、MCP 参数)和结果摘要(300 字符)。谁能读你的文件系统,谁就能读这本账——当成 shell history 对待。
  • 幽灵提醒是会话内的:一个会话只会看到自己那些未确认的调用。
  • 日志不会自动清理。想设上限就自己删,比如只留最近的 5 万行:
sqlite3 ~/.hermes/plugin-data/agent-ledger/ledger.db \
  "DELETE FROM ledger WHERE id NOT IN (SELECT id FROM ledger ORDER BY id DESC LIMIT 50000)"
  • 想每天收到一份姿态报告,posture_daily.py 需要你自己挂到 cron 上,并且用它自己的机器人凭证(环境变量 POSTURE_TELEGRAM_BOT_TOKEN / POSTURE_TELEGRAM_CHAT_ID)——它不会去读 Hermes 的 .env,也不会复用你的机器人令牌。

和已有做法的关系

Hermes 自己的定时任务也有一层「凭据存活」的保护:任务被谁领走、卡在什么状态、多久没动静,都有对应的机制(我们在中轮压缩完整性和定时任务那条线里写过)。agent-ledger 补的是另一个角度:它不关心某个任务的状态,它关心「有没有一次写入动作没被确认」,而且覆盖的不只是 cron,而是所有会改状态的工具。两者不冲突,装它也不会改变 Hermes 的行为——除非你显式打开策略拦截。

小结

  • 「操作没发生」和「操作发生了但没人知道」是两种事故,后者的代价经常更高,因为它不会被发现。
  • 一张 PENDING → CONFIRMED 的写前日志,加上宽限期判断和会话内的上下文注入,就能把幽灵调用变成一句「先核实再相信」。
  • 姿态打分是附赠的审计视角,但账本由被观察者自己写——把它当作卫生指标。
  • 拦截是可选的、签名式的,作者自己标注了绕过方式;先 dry_run 几天,看看它到底能挡住什么,再决定是否 enforce。