Schedule
周期性工作由 Harness 创建,而不是模型记住。
schedule_add / schedule_list / schedule_delete。
问题
让模型"记住 10 分钟后提醒我"?模型没有时钟,它下一次 step 才会行动——而 10 分钟后它可能早就结束回合了。
"稍后要做的事"不该依赖模型的记忆,而该由 Harness 的调度器拥有:一条持久的提醒记录,时间到了自动变成一条新的对话消息,回到原会话。
解决方案
调度记录 = 规则(after 延迟 / at 绝对时间 / every 周期)+ 提醒内容,持久化进会话日志;一个进程内定时器在到期时把提醒作为普通对话消息注入原会话。
const schedules = []; // 持久化:写入 s09 的事件日志
let nextId = 1;
define("schedule_add", {
type: "object",
properties: {
prompt: { type: "string" },
after_seconds: { type: "integer", minimum: 1 },
at: { type: "string" }, // RFC 3339 绝对时间
every_seconds: { type: "integer", minimum: 300 }, // 周期 ≥ 5 分钟
},
required: ["prompt"],
}, (args) => {
const rule = args.after_seconds ? { kind: "after", afterSeconds: args.after_seconds }
: args.at ? { kind: "at", at: args.at }
: args.every_seconds ? { kind: "every", everySeconds: args.every_seconds }
: null;
if (!rule) return "必须提供 after_seconds / at / every_seconds 之一";
const scheduledAt = computeTarget(rule); // 统一折算成 RFC 3339 UTC
const rec = { id: `schedule-${nextId++}`, prompt: args.prompt, ...rule, scheduledAt };
schedules.push(rec); append("schedule/change", rec);
return JSON.stringify(rec);
});
工作原理
第 1 步:规则统一折算为绝对目标时间——延迟、绝对时间、周期都归一成 scheduledAt(RFC 3339 UTC),定时器只认一个字段。
第 2 步:定时器检查到期记录,把提醒内容变成一条对话消息注入会话:
setInterval(() => {
const now = Date.now();
for (const rec of schedules) {
if (rec.dispatched || new Date(rec.scheduledAt).getTime() > now) continue;
rec.dispatched = true;
enqueueFollowup(`[提醒] ${rec.prompt}`); // 进入会话的普通输入队列
if (rec.kind === "every") { /* 锚定对齐:按固定间隔排下一次 */ }
}
}, 1000);
第 3 步:到期工作通过普通对话通道回来——模型下一次 step 会看到提醒消息,就像用户说了句话。调度器不直接调模型、不另开会话、不产生外部通知渠道:它只是"到了时间,往会话里放一条消息"。
为什么 every 周期 ≥ 5 分钟?因为提醒回到对话是协作的(要模型处理),高频打扰会污染会话。周期性工作由 Harness 创建、维护、重排——模型只负责创建时描述一次。
试一下
运行 s14:
- "10 秒后提醒我检查测试结果"(用
after_seconds: 10便于观察) - 在提醒到达前结束回合——观察提醒在下一次对话时出现
- 创建一个
every提醒,观察它按固定节奏重复
观察重点:提醒到达后模型做什么?如果会话当时没有活跃模型(回合已结束),提醒去哪了?(DSH 的答案:等会话重新活跃时补投递。)
以下内容基于 packages/schedule 与 docs/subsystems/schedule.md 的核查。DSH 的调度(dsh-schedule)设计要点:
一、持久状态在会话日志
- 只有
schedule/change事件流是持久的调度状态;定时器、空闲等待者都是可丢弃的投影。恢复会话 = 折叠日志。 - fork 的会话不会继承父会话的活跃提醒——每个会话的调度独立。
二、到期投递是"对话式的"
- 到期处理重查墙钟与活跃所有者,通过公开的 Agent 通道认领维护阶段,构造完整的提醒框架后调用
followup()入队——提醒成为普通对话轮次,不产生收据、外部通知或副作用承诺。 - 显式时区边界:目标时间在浏览器本地解释,Harness 只存 UTC。
三、你的最小实现 vs DSH
| 你的最小实现 | DSH 的真实实现 |
|---|---|
schedules 内存数组 |
schedule/change 事件流 + 折叠派生 |
setInterval 轮询 |
分段定时器 + 活跃根 Agent 唯一所有者 |
| 提醒直接入队 | 先验证到期与所有者,再构造完整框架入队(失败不追加) |
一句话:调度 = "未来要发生的事"的持久化 + 到期投递。DSH 让投递走会话自己的对话通道,因此提醒与普通消息享有同样的持久化、恢复与 UI 语义。