247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行

2026.06.30

·技术harnessx

20260630_5.webp|1152

重点:定时任务到点后,变成一条新的 Agent 输入

20260630_4.webp|864

  • 第 14 课当前要做的是:
    • 用户不敲回车。
    • 时间到了。
    • HarnessX 自动把一段任务送进 Agent Loop。
  • 这节课新增 3 个主 Agent 工具:
    • schedule_cron:登记未来任务。
    • list_crons:查看已登记任务。
    • cancel_cron:取消已登记任务。
  • 重点是看懂这条路:
    • 先登记未来任务。
    • 常驻进程负责等时间。
    • 时间到了先进队列。
    • Agent 空闲后再交付。
    • 最后变成一条 [Scheduled] 用户消息。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 1

  • 到点以后,cron 不会直接写日报。
  • cron 只会把下面这类消息塞进会话:
bash
[Scheduled] 现在生成今天的 832X 项目日报:
先调用 bash 执行 date 看当前时间;
再调用 bash 执行 git status --short 看工作区现场;
再调用 bash 执行 git diff --stat 看未提交改动范围;
再调用 bash 执行 git log --oneline --since="today 00:00" --max-count=20 看当天提交;
最后根据这些真实代码证据,输出一份简短日报:
今天做了什么、改了哪些关键文件、当前还剩什么尾巴、明天第一步是什么。
  • 后面还是原来的 Agent Loop:
bash
messages
 DeepSeek
 tool_calls
 本地 bash
 role: tool 回填
 DeepSeek 总结日报

具体案例:每天下班前,Agent 自动根据 832X 代码现场写日报

  • 先想一个真实场景:
    • 快下班了。
    • 我想知道今天在 832X 里到底做了什么。
    • 我不想靠脑子回忆。
    • 我希望 Agent 自己看代码现场,然后写一份日报。
  • 这件事靠人手动做,大概是:
    • 先看现在几点。
    • 再看工作区有没有未提交改动。
    • 再看今天改动了哪些文件。
    • 再看今天有没有提交。
    • 最后把这些证据整理成日报。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 2

  • 为了马上验证,先把它写成每分钟触发:
bash
cron="* * * * *"
  • 真实语义可以换成:
bash
# 每天 18:00 自动写日报
cron="0 18 * * *"
  • 这节课真正重要的不是 * * * * *
  • 真正重要的是这个 prompt
bash
现在生成今天的 832X 日报:
先调用 bash 执行 date 看当前时间;
再调用 bash 执行 git status --short 看工作区现场;
再调用 bash 执行 git diff --stat 看未提交改动范围;
再调用 bash 执行 git log --oneline --since="today 00:00" --max-count=20 看当天提交;
最后根据这些真实代码证据,输出一份简短日报:
今天做了什么、改了哪些关键文件、当前还剩什么尾巴、明天第一步是什么。

日报案例的一来一回

  • 用户先让 Agent 登记定时任务。
  • 模型调用 schedule_cron
  • HarnessX 本地只保存任务定义。
  • 到点以后,调度器把日报任务交给共享会话。
  • 模型再自己决定调用哪些 bash 命令。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 3

  • 读懂这张图,就读懂第 14 课:
    • 定时器不是日报生成器。
    • 定时器只是到点送来一条任务
    • 真正写日报的还是 Agent Loop。

第 12、13、14 课各自站在哪一层

  • 第 12 课 Task System:
    • 管“项目里有哪些任务、状态是什么、依赖有没有挡住”。
  • 第 13 课 Background Tasks:
    • 管“已经启动的慢命令,怎样在后台跑完再通知回来”。
  • 第 14 课 Cron Scheduler:
    • 管“未来某个时间,怎样自动生产一条 Agent 输入”。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 4

  • 用日报案例对齐一下:
    • 第 14 课负责:
      • 每天 18:00 把“写日报”送进 Agent Loop。
    • Agent Loop 负责:
      • 决定调用 dategit statusgit diffgit log
    • 第 13 课负责:
      • 如果某个命令很慢,可以后台跑,完成后再通知回来。

使用层面只有两个入口

  • hx agent
    • 无参数。
    • 启动常驻交互会话。
    • 同时启动定时检查和队列交付。
    • 想让日报真的到点自动执行,必须开这个。
  • hx agent "..."
    • 带任务文本。
    • 只执行这一轮
    • 可以创建、查看、取消 cron 定义。
    • 执行完进程退出,不会继续自动调度。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 5

  • 所以这节课的使用判断很简单:
bash
# 想观察日报到点自动触发
hx agent

# 只想单轮问一句,或者管理 cron 定义
hx agent "调用 list_crons 列出当前定时任务"

本课新增了三个主 Agent 工具

  • schedule_cron
    • 创建定时任务。
    • 保存 cronpromptrecurringdurable
    • 在日报案例里,它只负责登记“下班前写日报”这件事。
    • 它不会立刻执行 dategit
  • list_crons
    • 列出当前进程里已注册的 cron 任务。
    • durable 任务重启后会从 .scheduled_tasks.json 恢复。
  • cancel_cron
    • 根据任务 ID 取消一个 cron 任务。
    • 取消后,这个任务就不会再到点进入队列。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 6

  • 工具边界:
    • 这 3 个工具只给主 Agent。
    • 子代理不继承。
    • 子代理仍然只做局部工作,不维护父会话的定时队列。

常驻会话里怎么通信

  • 常驻模式里只有一份共享会话
  • 手动输入和定时任务不是两套 Agent。
  • 它们最后都进入同一个 messages

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 7

  • 手动输入:
js
messages.push({
  role: "user",
  content: "列出当前目录里的文件",
});
  • 定时日报输入:
js
messages.push({
  role: "user",
  content: `[Scheduled] 现在生成今天的 832X 日报:...`,
});
  • 这就是会话通信的核心:
    • 定时任务没有特殊执行通道。
    • 它只是换了一个来源。
    • 最后仍然变成 role: "user"

为什么需要队列和 busy 锁

  • 问题不是“时间怎么匹配”。
  • 问题是:
    • 到点时,Agent 可能正在处理用户刚输入的任务。
    • 如果定时任务也同时改 messages,上下文会乱。
  • 所以第 14 课拆成两步:
    • 时间到了,先进队列。
    • Agent 空闲,再交付。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 8

  • busy 锁只表达一个判断:
    • 当前有没有一个 Agent turn 正在改 messages
  • 有:
    • 定时任务等着。
  • 没有:
    • 队列交付。

登记日报任务时发生什么

  • 用户在常驻会话里输入自然语言。
  • 模型调用 schedule_cron
  • 本地工具保存五个字段。
  • 这一步不会立刻写日报。
bash
请调用 schedule_cron 创建一个 session-only 周期任务,
cron="* * * * *",
prompt="现在生成今天的 832X 日报:
先调用 bash 执行 date 看当前时间;
再调用 bash 执行 git status --short 看工作区现场;
再调用 bash 执行 git diff --stat 看未提交改动范围;
再调用 bash 执行 git log --oneline --since=\"today 00:00\" --max-count=20 看当天提交;
最后根据这些真实代码证据,输出一份简短日报:
今天做了什么、改了哪些关键文件、当前还剩什么尾巴、明天第一步是什么。",
recurring=true,
durable=false。
  • 保存下来的任务像这样:
js
{
  id: "cron_...",
  cron: "* * * * *",
  prompt: "现在生成今天的 832X 日报:先调用 bash 执行 date 看当前时间;再调用 bash 执行 git status --short 看工作区现场;再调用 bash 执行 git diff --stat 看未提交改动范围;再调用 bash 执行 git log --oneline --since=\"today 00:00\" --max-count=20 看当天提交;最后根据这些真实代码证据,输出一份简短日报:今天做了什么、改了哪些关键文件、当前还剩什么尾巴、明天第一步是什么。",
  recurring: true,
  durable: false,
}
  • 这里要记住:
    • cron 决定什么时候触发。
    • prompt 决定到点后交给 Agent 的任务。
    • recurring 决定是否重复。
    • durable 决定是否写入 .scheduled_tasks.json

到点写日报时发生什么

  • 到点以后,不再需要用户输入。
  • 调度器把日报任务送进队列。
  • 队列等 Agent 空闲。
  • 空闲后,把日报 prompt 包成 [Scheduled] 消息。
  • 模型根据这条消息调用真实 bash。

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 9

  • 这张图的关键判断:
    • Scheduler 只管“到点”。
    • Queue 只管“先放着”。
    • Agent Session 只管“安全写入 messages”。
    • Agent Loop 负责“让模型看证据并写日报”。

cron 语法只讲能跑懂这一课的三个例子

bash
# 课堂验证:每分钟触发一次
* * * * *

# 真实日报:每天 18:00 触发一次
0 18 * * *

# 工作日日报:周一到周五 18:00 触发
0 18 * * 1-5
  • 当前实现还支持:
    • *
    • */N
    • 固定数字
    • 范围
    • 逗号列表
  • 但这不是本文主线。
  • 只要记住:
bash
cron 匹配当前本地时间
 日报任务进入队列
 Agent 空闲后收到 [Scheduled] 输入

durable 和 session-only 的边界

bash
# 课堂验证用这个:退出 hx agent 后任务消失
durable=false

# 想让任务定义跨重启,就用这个
durable=true
  • durable 只保证任务定义写进 .scheduled_tasks.json
  • durable 不保证:
    • 电脑关机期间自动执行。
    • hx agent 没启动时自动执行。
    • 进程退出期间错过的时间补跑。
  • 如果要系统级准点执行:
    • 那是 crontablaunchd 或 daemon 的问题。
    • 不属于这节教学版。

怎么跑

  • 沿用第 0 课的 npm link 和 DeepSeek 配置。
  • 先启动常驻会话:
bash
hx agent
  • 看到类似输出:
bash
HarnessX agent
输入任务后按回车,输入 /exit /quit 退出。
Cron 只会在这个常驻进程运行期间自动触发。

[CRON] scheduler started
[CRON] queue processor started
hx agent>
  • 在会话里输入:
bash
请调用 schedule_cron 创建一个 session-only 周期任务,
cron="* * * * *",
prompt="现在生成今天的 832X 日报:
先调用 bash 执行 date 看当前时间;
再调用 bash 执行 git status --short 看工作区现场;
再调用 bash 执行 git diff --stat 看未提交改动范围;
再调用 bash 执行 git log --oneline --since=\"today 00:00\" --max-count=20 看当天提交;
最后根据这些真实代码证据,输出一份简短日报:
今天做了什么、改了哪些关键文件、当前还剩什么尾巴、明天第一步是什么。",
recurring=true,
durable=false。
  • 为什么这个例子能验证第 14 课:

    • * * * * *
      • 每分钟触发,等待时间短。
    • durable=false
      • 只存在当前进程,不污染下次运行。
    • 日报 prompt:
      • 会迫使模型调用真实 bash。
      • 不是让模型凭空编日报。
  • 关键日志顺序:

bash
# 模型先调用工具登记任务
> schedule_cron cron=* * * * * recurring=true durable=false
[CRON] scheduled cron_... '* * * * *' -> 现在生成今天的 832X 日报...

# 到点后,调度器只负责把任务放进队列
[CRON] fired cron_... -> 现在生成今天的 832X 日报...

# Agent 空闲后,队列交付给共享会话
[CRON] queue processor delivering jobs=1
[CRON] injected cron_...: 现在生成今天的 832X 日报...

# 后面回到普通 Agent Loop,模型调用真实 bash
$ date
Tue Jun 30 18:00:01 CST 2026

$ git status --short
 M README.md
 M src/agent-loop.js
?? src/cron.js

$ git diff --stat
 README.md         |  ...
 src/agent-loop.js |  ...
 src/cron.js       |  ...

$ git log --oneline --since="today 00:00" --max-count=20
7ea1444 chore(832x): 同步实验台状态
  • 验收标准:
bash
登记成功
 到点 fired
 队列 delivering
 注入 [Scheduled]
 模型调用 date
 模型调用 git status
 模型调用 git diff
 模型调用 git log
 模型根据真实代码证据输出日报
  • 取消任务:
bash
请调用 list_crons 找到刚才的任务,
再调用 cancel_cron 取消它,
最后再次调用 list_crons 确认已为空。
  • 退出常驻会话:
bash
/exit

这一课真正要记住

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 10

  • CronJob:
    • 一条未来任务定义。
    • 保存时间、prompt、是否重复、是否持久化。
  • Scheduler:
    • 看时间。
    • 不写日报。
  • Queue:
    • 保存已经到点但还没交付的日报任务。
  • Queue Processor:
    • 等 Agent 空闲后交付。
  • Agent Session:
    • 维护共享 messages
    • 保护手动输入和定时输入不要同时写上下文。
  • Agent Loop:
    • 收到 [Scheduled]
    • 请求模型。
    • 执行真实 bash。
    • 根据工具结果写日报。

一句话:

bash
 14 课不是让 cron 直接写日报,而是让 cron 到点生产一条新的 Agent 输入。

源码

源码只看主链路。

源码大流程图

247. 技术:HarnessX 第 14 课:让定时任务到点自动进入 Agent Loop,用调度器、队列和空闲交付拆开时间与执行 图表 11

代码概览

入口只做分流。

js
// hx agent          -> 常驻会话
// hx agent "任务"   -> 单轮任务
if (command === "agent") {
  const task = rest.join(" ").trim();

  if (!task) {
    await 启动常驻代理会话();
    return;
  }

  await 执行单轮代理任务(task);
  return;
}

常驻会话同时启动三件事。

js
async function 启动常驻代理会话() {
  const 共享会话 = 创建代理会话();

  启动定时运行时({
    可以交付() {
      return !共享会话.正在忙();
    },
    async 交付(到期任务) {
      await 共享会话.处理定时任务(到期任务);
    },
  });

  // 终端每输入一行,也交给同一个共享会话。
}

共享会话把两种输入写进同一个 messages

js
function 创建代理会话() {
  const messages = [];
  let busy = false;

  async function 处理用户输入(text) {
    等待当前任务结束();
    busy = true;
    messages.push({ role: "user", content: text });
    await 进入代理循环(messages);
    busy = false;
  }

  async function 处理定时任务(jobs) {
    if (busy) return false;
    busy = true;

    for (const job of jobs) {
      messages.push({
        role: "user",
        content: `[Scheduled] ${job.prompt}`,
      });
    }

    await 进入代理循环(messages);
    busy = false;
    return true;
  }
}

代码细分

这一课的源码细节压成一条链。

js
// 用户说:帮我创建下班前日报任务。
工具 schedule_cron({
  cron: "* * * * *",
  prompt: "现在生成今天的 832X 日报:先调用 bash 执行 date 看当前时间;再调用 bash 执行 git status --short 看工作区现场;再调用 bash 执行 git diff --stat 看未提交改动范围;再调用 bash 执行 git log --oneline --since=\"today 00:00\" --max-count=20 看当天提交;最后根据这些真实代码证据,输出一份简短日报:今天做了什么、改了哪些关键文件、当前还剩什么尾巴、明天第一步是什么。",
  recurring: true,
  durable: false,
});

// 工具只保存定义,不写日报。
任务表.add({
  id: "cron_...",
  cron: "* * * * *",
  prompt: "现在生成今天的 832X 日报...",
  recurring: true,
  durable: false,
});

// 时间检查器只负责发现“到点了”。
每秒检查一次时间(() => {
  if (当前时间匹配任务.cron && 本分钟还没触发过) {
    到期队列.push(任务);
  }
});

// 队列交付器只在 Agent 空闲时行动。
每 200ms 检查一次(() => {
  if (到期队列不为空 && Agent 当前空闲) {
    const jobs = 取出队列里的任务();
    共享会话.处理定时任务(jobs);
  }
});

// 共享会话把日报任务变成普通 user message。
function 处理定时任务(jobs) {
  for (const job of jobs) {
    messages.push({
      role: "user",
      content: `[Scheduled] ${job.prompt}`,
    });
  }

  进入代理循环(messages);
}

Agent Loop 本身没有变成日报生成器。

它只是多接收了一种用户消息。

js
// 普通用户输入
{ role: "user", content: "列出当前目录" }

// 定时日报输入
{
  role: "user",
  content: "[Scheduled] 现在生成今天的 832X 日报:先调用 bash 执行 date 看当前时间;再调用 bash 执行 git status --short 看工作区现场;再调用 bash 执行 git diff --stat 看未提交改动范围;再调用 bash 执行 git log --oneline --since=\"today 00:00\" --max-count=20 看当天提交;最后根据这些真实代码证据,输出一份简短日报:今天做了什么、改了哪些关键文件、当前还剩什么尾巴、明天第一步是什么。"
}

后面仍然是旧链路:

bash
更新 system prompt
 请求 DeepSeek
 如果模型返回 tool_calls,就执行本地工具
 role=tool 结果写回 messages
 继续请求模型
 没有工具调用时输出最终回答

第 14 课的关键改动到这里就结束了。

bash
未来时间
 到期队列
 空闲交付
 [Scheduled] 日报消息
 普通 Agent Loop
 读取 git 证据
 写出日报