【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 10 篇】
执行环境抽象与定时任务让命令在任意地方跑、让 Agent 在未来主动干活导读命令只能在本地跑Agent 只能被动响应本篇拆解 Hermes 阶段 3 收官两章用 snapshot 文件传递状态让命令在本地/Docker/SSH 任意环境执行用任务即消息机制让 Agent 到点自动开工。附完整代码与 9 个初学者常踩的坑。两个主动需求产品经理又来了。“能不能让 agent 在 Docker 容器里跑命令”“30 分钟后帮我看看部署成功没。”“每 2 小时查一下磁盘。”“工作日早上 9 点总结 git log。”前 9 篇我们手搓的 Hermes Agentterminal 工具只有 4 行subprocess.run直接在本地跑。定时任务完全没有——agent 只能被动等用户说话。本篇解决这两个问题命令在哪跑s14 执行环境抽象到点自己动s15 定时任务。这也是阶段 3 的最后一章。前半·执行环境if-else 会爆炸两个问题状态不连续 分支爆炸先看现状。s01-s11 的 terminal 工具长这样subprocess.run(command,shellTrue)每次调用都是全新的 bash 进程。cd /tmp之后下一条命令又回到默认目录。export FOObar之后下一条命令echo $FOO是空的。状态不连续。产品说支持 Docker你写ifbackenddocker:subprocess.run([docker,exec,...])elifbackendssh:subprocess.run([ssh,host,...])elifbackendk8s:...六个后端六个分支。每个分支还要处理状态传递、错误处理、超时控制。代码爆炸。而且状态问题更严重Docker 里cd了下一条命令又回到/。为什么不维护长驻 bash你可能会想那我不每次启动新 bash维护一个常驻进程不就行了三个坑等着你。坑一输出边界。没有分隔符你不知道哪段输出对应哪条命令。得手动塞echo __MARKER__做标记。坑二卡死传染。cat命令在等输入整个 bash 卡住。你想单独杀掉这条命令做不到——进程是同一个。坑三多行命令。for循环要等它执行完才知道什么时候读输出。你得懂 bash 语法才能判断现在该不该读。三个坑每一个都够你调一晚上。Hermes 方案snapshot 文件传递状态每条命令仍然启动新 bash但通过两个文件传递状态self._session_iduuid.uuid4().hex[:12]self._snapshot_pathf/tmp/hermes-snap-{self._session_id}.sh# 环境变量self._cwd_filef/tmp/hermes-cwd-{self._session_id}.txt# 工作目录核心是五行包装脚本source snapshot.sh ← 恢复上次保存的环境变量 cd {上次的工作目录} ← 恢复上次的目录 {用户的命令} ← 真正要执行的 export -p snapshot.sh ← 保存新的环境变量 pwd -P cwd.txt ← 保存新的工作目录每次执行完把新的状态存回文件。下一条命令启动时先恢复再执行。看真实代码_wrap_commanddef_wrap_command(self,command:str)-str:parts[]ifself._snapshot_ready:parts.append(fsource{self._snapshot_path}2/dev/null)parts.append(fcd{shlex.quote(self.cwd)}2/dev/null)parts.append(command)parts.append(f_exit$?; export -p {self._snapshot_path}2/dev/null; fpwd -P {self._cwd_file}2/dev/null; exit $_exit)return; .join(parts)注意_exit$?这个细节。先存退出码再保存 snapshot。如果顺序反了export -p的退出码永远是 0会覆盖用户命令的退出码。到时候你的 agent 执行ls /nonexist都返回成功。三种后端本地 / Docker / SSH基类叫BaseExecutionEnvironment注意不是文档里的 BaseEnvironment。子类只需实现_run_bash(cmd_string)和cleanup()。LocalBackend直接subprocess.Popen([bash, -c, cmd_string])。但有个关键细节——密钥过滤_SECRET_BLOCKLIST# 过滤 OPENAI_API_KEY 等防止 agent 执行env | grep KEY拿到你的 API key。Docker 和 SSH 后端不需要这个——容器和远程机器天然没有你的密钥。DockerBackend先docker run -d长驻容器sleep infinity每条命令docker exec。为什么要长驻因为 snapshot 文件在容器文件系统里。容器销毁snapshot 就丢了。安全参数是重点--cap-drop ALL # 防 mount / 改网络 --security-opt no-new-privileges # 防提权 --pids-limit 256 # 防 fork 炸弹 --memory 512m # 内存上限SSHBackendControlMasterautoControlPathControlPersist300。连接复用30 条命令不用 30 次握手。BatchModeyes不弹交互提示。一行配置切后端terminal:backend:docker# local / docker / ssh工具代码一行不用改。远程后端读 cwd 文件多一步docker exec cat/ssh cat仅此而已。教学边界这层只管 terminal 工具。read_file不走这层——它没有跨命令状态open(path)改docker exec cat path即可。过渡能跑了但只能被动响应执行环境搞定了。命令可以在任意地方跑状态连续切换后端一行配置。但 agent 还是只能被动响应。用户不说它不动。“30 分钟后帮我看看部署成功没”——30 分钟后谁来看后半·定时任务让 Agent 到点自己动核心洞察任务即消息三个需求——延时、间隔、cron——一个共同点在未来某个时间主动做事。Hermes 的方案定时任务不是新的执行路径。它只是一个会在未来某个时间假装用户说了句话的机制。从GatewayRunner往下全部复用。工具、错误恢复、权限、委派——都自动生效。看真实代码dataclassclassCronJob:job_id:strschedule:str# 30m / every 2h / 0 9 * * 1-5prompt:str# 触发时发给 agent 的消息session_key:strcreated_at:strnext_fire:floatone_shot:bool# True delete after firing到期后构造一个MessageEventtextjob.prompt,platformcron,chat_id原用户 session_key送进GatewayRunner._handle_message。三个好处全部能力免费复用——工具、错误恢复、权限、委派执行历史自动持久化——SQLite 记录回复自动路由回原平台——用户在哪聊的结果回哪去三种调度格式格式示例含义one_shot延时30m30 分钟后一次True间隔every 2h每 2 小时循环FalseCron0 9 * * 1-5工作日 9 点False为什么不只用 cron因为用户说30 分钟后的频率远高于设个 cron。延时格式一行搞定parse_schedule(30m)。parse_schedule逻辑every前缀 → 循环间隔裸时长 → 一次性延时否则 → cron 表达式。_UNITS{s:1,m:60,h:3600,d:86400}_next_cron_fire五字段从下一分钟逐分钟找最多 366 天。注意 Pythonweekday()0Mon vs cron 0Sun 的转换——这个坑踩的人最多。JobStorejobs.json 锁持久化用jobs.json不用 SQLite。任务少、人可读、不需要全文搜索。threading.Lock()# 主线程写 后台线程读get_due返回到期任务advanceone_shot 直接 pop循环任务重新parse_schedule。JobScheduler后台线程 30 秒扫一次daemonTrue# 后台线程whileTrue:forjobinget_due():fire(job)advance(job)time.sleep(30)cron_toolaction create/list/delete闭包绑定 store session_key。CLI 模式fire_callback直接调run_conversation()没有 Gateway 的场景。避坑9 个初学者常踩的坑两章合并精选每一个都是真实事故。执行环境 4 错忘了过滤 API key——agent 执行env | grep KEY直接拿到密钥Docker 不加资源限制——fork 炸弹 / 内存打爆宿主机SSH 每条命令建新连接——30 条命令 30 次握手慢到怀疑人生命令包装用串联——用户命令非零时 export 不执行snapshot 断掉定时任务 5 错在主线程检查定时任务——input()阻塞定时任务永远不触发忘了加锁——字典遍历时大小改变RuntimeError一次性任务不删除——30 秒后又触发重复执行cron 星期几搞错——Python 0Mon vs cron 0Sun差一天没处理 jobs.json 损坏——原子写 JSONDecodeError降级阶段 3 收官多平台接收s12-s13 跨环境执行s14 定时主动做事s15。你的 agent 现在能接住任何平台的任何消息在任意环境执行命令到点自己开工。阶段 4 开始高级能力MCP、浏览器、语音视觉。下一篇第 11 篇拆解高级能力扩展继续关注。你在做定时任务时踩过哪个坑欢迎留言交流。参考文献Hermes Agent 教学仓库agents/s14_terminal_backends.py本文代码素材72864 字节真实可运行Hermes Agent 教学仓库agents/s15_scheduled_tasks.py本文代码素材90769 字节真实可运行Hermes Agent 教学仓库docs/zh/s14-terminal-backends.md与docs/zh/s15-scheduled-tasks.mdsnapshot 机制、任务即消息详解源码获取如需本系列全部源码请在以下链接克隆https://gitcode.com/ganxin7932508/learn-hermes-agent.git

相关新闻

最新新闻

日新闻

周新闻

月新闻