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

- 第 14 课当前要做的是:
- 用户不敲回车。
- 时间到了。
- HarnessX 自动把一段任务送进 Agent Loop。
- 这节课新增 3 个主 Agent 工具:
schedule_cron:登记未来任务。list_crons:查看已登记任务。cancel_cron:取消已登记任务。
- 重点是看懂这条路:
- 先登记未来任务。
- 常驻进程负责等时间。
- 时间到了先进队列。
- Agent 空闲后再交付。
- 最后变成一条
[Scheduled]用户消息。
- 到点以后,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 自己看代码现场,然后写一份日报。
- 这件事靠人手动做,大概是:
- 先看现在几点。
- 再看工作区有没有未提交改动。
- 再看今天改动了哪些文件。
- 再看今天有没有提交。
- 最后把这些证据整理成日报。
- 为了马上验证,先把它写成每分钟触发:
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 命令。
- 读懂这张图,就读懂第 14 课:
定时器不是日报生成器。- 定时器只是到点送来一条任务。
- 真正写日报的还是 Agent Loop。
第 12、13、14 课各自站在哪一层
- 第 12 课 Task System:
- 管“项目里有哪些任务、状态是什么、依赖有没有挡住”。
- 第 13 课 Background Tasks:
- 管“已经启动的慢命令,怎样在后台跑完再通知回来”。
- 第 14 课 Cron Scheduler:
- 管“未来某个时间,怎样自动生产一条 Agent 输入”。
- 用日报案例对齐一下:
- 第 14 课负责:
- 每天 18:00 把“写日报”送进 Agent Loop。
- Agent Loop 负责:
- 决定调用
date、git status、git diff、git log。
- 决定调用
- 第 13 课负责:
- 如果某个命令很慢,可以后台跑,完成后再通知回来。
- 第 14 课负责:
使用层面只有两个入口
hx agent:- 无参数。
- 启动常驻交互会话。
- 同时启动定时检查和队列交付。
- 想让日报真的到点自动执行,必须开这个。
hx agent "...":- 带任务文本。
- 只执行这一轮。
- 可以创建、查看、取消 cron 定义。
- 执行完进程退出,不会继续自动调度。
- 所以这节课的使用判断很简单:
bash
# 想观察日报到点自动触发
hx agent
# 只想单轮问一句,或者管理 cron 定义
hx agent "调用 list_crons 列出当前定时任务"本课新增了三个主 Agent 工具
schedule_cron:- 创建定时任务。
- 保存
cron、prompt、recurring、durable。 - 在日报案例里,它只负责登记“下班前写日报”这件事。
- 它不会立刻执行
date或git。
list_crons:- 列出当前进程里已注册的 cron 任务。
- durable 任务重启后会从
.scheduled_tasks.json恢复。
cancel_cron:- 根据任务 ID 取消一个 cron 任务。
- 取消后,这个任务就不会再到点进入队列。
- 工具边界:
- 这 3 个工具只给主 Agent。
- 子代理不继承。
- 子代理仍然只做局部工作,不维护父会话的定时队列。
常驻会话里怎么通信
- 常驻模式里只有一份共享会话。
- 手动输入和定时任务不是两套 Agent。
- 它们最后都进入同一个
messages。
- 手动输入:
js
messages.push({
role: "user",
content: "列出当前目录里的文件",
});- 定时日报输入:
js
messages.push({
role: "user",
content: `[Scheduled] 现在生成今天的 832X 日报:...`,
});- 这就是会话通信的核心:
- 定时任务没有特殊执行通道。
- 它只是换了一个来源。
- 最后仍然变成
role: "user"。
为什么需要队列和 busy 锁
- 问题不是“时间怎么匹配”。
- 问题是:
- 到点时,Agent 可能正在处理用户刚输入的任务。
- 如果定时任务也同时改
messages,上下文会乱。
- 所以第 14 课拆成两步:
- 时间到了,先进队列。
- Agent 空闲,再交付。
- busy 锁只表达一个判断:
- 当前有没有一个 Agent turn 正在改
messages。
- 当前有没有一个 Agent turn 正在改
- 有:
- 定时任务等着。
- 没有:
- 队列交付。
登记日报任务时发生什么
- 用户在常驻会话里输入自然语言。
- 模型调用
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。
- 这张图的关键判断:
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没启动时自动执行。- 进程退出期间错过的时间补跑。
- 如果要系统级准点执行:
- 那是
crontab、launchd或 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这一课真正要记住
- CronJob:
- 一条未来任务定义。
- 保存时间、prompt、是否重复、是否持久化。
- Scheduler:
- 看时间。
- 不写日报。
- Queue:
- 保存已经到点但还没交付的日报任务。
- Queue Processor:
- 等 Agent 空闲后交付。
- Agent Session:
- 维护共享
messages。 - 保护手动输入和定时输入不要同时写上下文。
- 维护共享
- Agent Loop:
- 收到
[Scheduled]。 - 请求模型。
- 执行真实 bash。
- 根据工具结果写日报。
- 收到
一句话:
bash
第 14 课不是让 cron 直接写日报,而是让 cron 到点生产一条新的 Agent 输入。源码
源码只看主链路。
源码大流程图
代码概览
入口只做分流。
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 证据
→ 写出日报