程云来 / 杭州
返回文章·Agent 与 Harness
·约 5 分钟

思想|Cron Scheduler,没人再发消息,下一轮从哪里来?

从每五分钟检查程序出发,区分时间表、触发队列和空闲交付:让宿主产生工作,让原循环处理工作。


模型说“记住了”,五分钟后谁来叫它?

“每五分钟检查一次 main.py,拿到结果再报告。”

模型回答“好的”,这轮响应就结束了。五分钟以后,如果没有新的调用,它不会自己从这句话里醒来。上一课的后台任务解决了程序已经启动之后如何等待;眼前的问题更早:没有新输入时,谁来产生下一份工作?

宿主保存一张时间表,到点产生一条待交付提示,再在 Agent 空闲时把提示交给原循环。时间检查、排队和执行分开以后,我们才能解释两种看似矛盾的现象:到点了却还没执行,执行结束了时间表却还在。

CRON FIELDSUTC+08:00AGENT LOOPMESSAGES / TOOLSTURN OUTCOME五分钟时间表宿主检查时间待交付提示空闲后的模型本次执行结果
先保存“什么时候、做什么”

用户要求每五分钟检查main.py。宿主登记*/5 * * * *和检查提示,当前没有启动程序,也没有模型响应。

  1. 先保存“什么时候、做什么”:用户要求每五分钟检查main.py。宿主登记*/5 * * * *和检查提示,当前没有启动程序,也没有模型响应。
  2. 09:00,宿主检查时间表:调度器读取固定UTC+08:00时钟,本分钟满足五分钟刻度。检查时间由宿主负责,不需要模型在上一轮记住一个计时器。
  3. 先入队,再记住本分钟:产生cron-1@01:00Z对应的提示,暂未交付。09:00:40再检查仍是同一个UTC分钟,因此不会多加一项。
  4. Agent空闲,才交付这一项:宿主占用一个轮次位置,将触发ID和检查提示组成新问题交给原循环。到点没有授予程序许可,运行仍经过原检查。
  5. 本次回答保存真实程序结果:获准运行才得到exit_code=0和42;拒绝也可以得到answered,但回答必须说明没有执行。轮次结果记录后释放占用。
  6. 时间表仍在,等待下一次匹配:一次回答结束不删除重复时间表。下一次匹配会产生新的事件;关闭本课进程则表和队列一起消失,没有后台服务继续运行。

到点,只是队列里多了一项

时间表保存“什么时候、做什么”。本例是表达式 */5 * * * *,以及“读取预期,获准后运行程序,报告本次结果”这段提示。cron 是按日历刻度匹配的时间表;这里的分钟刻度为 0、5、10,直到 55。

宿主每次检查当前时间,匹配后产生一个事件。事件带时间表 ID、触发分钟和提示。例如 cron-1@01:00Z 表示 cron-1 在 UTC 的 01:00 触发,对应本例固定 UTC+08:00 的 09:00。

时间表可以重复,事件只代表其中一次触发。 如果把二者当成同一个对象,第一次消费就可能把整张表删掉;如果只看时间表还在,又可能在同一分钟不断追加重复事件。

因此需要保存最后成功入队的分钟。09:00:01 和 09:00:40 属于同一个分钟,只产生一项;09:05 是另一项。标识包含日期,第二天的 09:00 不会被前一天挡住。

Agent还在工作,就先别插话

到点时,Agent 可能正在等模型响应,也可能正在执行工具。如果调度器直接改这轮 messages,模型当前到底看到了什么就又变得含糊了。

本课让调度器只写队列。消费者检查宿主是否还有其他工作、是否已有轮次占用;空闲才取一项,组成新问题,交给原来的 run_loop。正在进行的轮次保持自己的消息和预算,定时提示不会从响应中间插进来。

对象保存的内容发生变化的时机
时间表cron、提示、最后触发分钟登记、取消或成功入队后
触发队列某张表在某个分钟产生的事件到点追加,空闲时取首项
当前占用正在交付的事件 ID锁内占用,轮次返回后释放
轮次结果answered、call_limit 或异常等状态原循环返回或交付失败后

这个占用位置不是第十二课的持久任务负责人,也不是第十三课的后台进程 ID。它只负责保证本课消费者不会同时交付两轮。

排着队,也需要说明排队规则

假设 09:00 时 Agent 没有空闲,09:05 仍然没空。本例保留两条事件,空闲后依次交付。对“逐次检查”来说,这样容易核对;对“只看最新状态”来说,连续补做旧检查可能没有价值,此时应另加合并规则。

当前实现最多保留八条待交付事件。队列满时不提前写“本分钟已经触发”:如果本分钟释放了空间,下一次检查仍有机会入队。若等到之后的分钟才恢复,本课不会回头补发错过的时刻。去重、积压、满队列和停机补发分别是四个决策,不能靠一个 last_run 字段含糊带过。

一个轮次出错时,本课保存异常并释放占用,没有自动把事件放回队列。因为“没收到结论”不证明工具没有执行过;请求恢复可以沿用上一课已有机制,而整轮重跑需要另外核对副作用。

时间到了,许可也自动有了吗?

时间表保存的是待交付的工作要求,程序权限仍由执行入口决定。定时事件可以成功进入模型,也可以得到 answered,但回答内容可能是“没有许可,未运行”。判断程序是否运行,仍然要查本次工具结果。

本课把时间固定为 UTC+08:00,并只实现足以观察机制的五字段子集:*、单个数值,以及分钟字段的 */N。偏移始终保持八小时,也没有范围和列表语法。它按分钟刻度匹配,*/7 之类的表达式跨小时也不保证均匀相隔七分钟;需要固定经过时长时,应选择相应的间隔调度方式。

会话表与队列都保存在本进程内存中。关闭进程,时间检查就停止,重新运行也不会恢复旧表。若要求关闭应用后仍执行,应把唤醒职责交给持续运行的宿主或外部调度设施,再单独设计持久化、恢复和重复执行边界。

实践篇逐行展开“时间匹配 → 触发入队 → 空闲交付”,并用注入时钟验证完整路径,不必真的等五分钟。参考课程的 Cron Scheduler提供了这一拆分思路;Claude Code 的官方定时文档也说明提示先排队、在轮次之间交付。官方产品会继续演进,本文的会话内 Python 实现有自己的明确边界,不据此推断其内部源码。