核心命题:当任务单元从"进程"变成"对话",调度系统需要处理的不是时间管理,而是三个全新的问题:不确定性、信任边界和会话隔离。
传统 cron 调度的是进程。进程有明确的生命周期:启动、执行、退出,返回码为 0 或非 0,运行时间可以预测(基于历史数据),超时可以用信号实现(SIGKILL 之后什么都不剩)。
Hermes cron 调度的是对话。这个差别不只是"AI 有记忆"这么简单。
不确定的长度:一个处理邮件摘要的 cron 任务,正常情况下可能需要 2 次工具调用,30 秒完成。但如果有一封邮件涉及复杂的日程冲突,Agent 可能需要调用日历工具、搜索历史对话、调用委托任务——变成 15 次工具调用,5 分钟完成。这不是 bug,这是 AI Agent 的正常工作方式。传统 cron 的超时设为 60 秒,会错误地截断第二种情况。
不确定的结束信号:传统进程的结束是明确的(退出码)。AI Agent 的对话结束是什么?是 Agent 返回了一段文字就叫完成了——但如果 Agent 在无限循环里(每次调用一个工具,工具失败,重试,再失败)怎么办?语义上的"完成"和技术上的"进程退出"在 AI 语境下是分开的两件事。
需要优雅退出:强制杀进程之后,用户什么都看不到。但 AI Agent 被超时中断后,用户应该能看到一个说明性的总结:"任务超时,已完成的部分工作是 X,未完成的部分是 Y"。优雅退出和硬性截断是两个需要同时满足的需求——这在传统 cron 里根本不存在。
Hermes 的解决方案是**不活跃超时(inactivity timeout)**而不是绝对超时:
# scheduler.py
_cron_timeout = 600.0 # 默认 10 分钟不活跃超时
while True:
done, _ = concurrent.futures.wait({_cron_future}, timeout=_POLL_INTERVAL)
if done:
result = _cron_future.result()
break
# 检查不活跃时长
_idle_secs = agent.get_activity_summary().get("seconds_since_activity", 0.0)
if _idle_secs >= _cron_inactivity_limit:
_inactivity_timeout = True
break
Agent 每次工具调用、每次 API 调用、每次收到流式 token 都会更新活跃时间戳(_touch_activity())。调度器每 5 秒轮询一次,检查距上次活跃多久了。如果一个 Agent 在积极工作(大量工具调用),它可以运行数小时而不超时。只有当 Agent 卡住了(API 调用挂起、工具死锁、无限等待),超时才会触发。
超时后,调度器会收集 Agent 的活跃摘要(最后一次活跃的描述、当前正在运行的工具、已完成的迭代次数),构建一个诊断报告发给用户。这不是完全优雅的退出(无法让 Agent 写一个总结段落),但比"任务超时,无更多信息"要好得多。
jobs.py 和 scheduler.py 的分离:传递性价值cron/ 目录里有两个文件:jobs.py(作业存储和管理)和 scheduler.py(执行引擎)。这个分离不只是"更整洁",它有具体的传递性价值。
使用 jobs.py 但不需要 scheduler.py 的代码路径:
/cron list 和 hermes cron list:列出已调度的任务,只需要从文件读取并格式化,不需要任何执行引擎cronjob 工具:cronjob_tools.py 调用 jobs.py 里的 create_job()、update_job()、pause_job() 来管理任务,完全不需要 scheduler.pyjobs.py 的存储逻辑单独测试(解析、验证、序列化),而不需要启动完整的调度引擎如果 jobs.py 和 scheduler.py 合并在一起: