跳转到内容

输入关键词开始搜索文档。

定时调度

最后更新 查看 Markdown 源文件

定时调度以固定频率向智能体传递输入:cron 触发器触发,你的代码调用 dispatch(...) 向智能体对话发送消息。Flue 本身没有调度器——每个目标平台将其 cron 机制与每个其他传递使用的相同调度表面配对。本指南涵盖在 Node.js 和 Cloudflare 上声明调度、触发传递的消息、选择对话 ID、等待运行结果,以及围绕错过触发、重叠和持久性的操作行为。

调度的工作原理

调度有三个部分:

  1. 触发器。 在 Node.js 上,app.ts 中的进程内 cron 库;在 Cloudflare 上,Worker Cron 触发器;在托管平台上,平台自己的 cron 服务调用你的 HTTP 表面。
  2. 传递。 触发器的回调调用 dispatch(agent, { id, message })。当消息被持久地接收到对话队列时,调度会解析——它不等待模型运行。因为调度直接寻址注册的智能体函数,调度的智能体根本不需要 HTTP 挂载(参见仅调度智能体)。
  3. 对话。 你传递的 id 命名接收每次触发的对话。这是一个持续的对话还是每次运行一个新对话是一个设计选择,下文将介绍。

智能体本身是普通的——智能体函数中没有任何标记表明它是被调度的:

src/agents/reporter.ts
'use agent';
import { useModel } from '@flue/runtime';

export function Reporter() {
  useModel('anthropic/claude-sonnet-4-6');
  return '自主完成调度任务。';
}

当调度工作涉及应用控制的步骤——读取数据源、写入报告、调用外部 API——将这些步骤放在harness 工具后面,以便它们在每次触发时行为相同。

在 Node.js 上调度

在 Node 目标上,服务器进程是长期运行的,因此 app.ts 模块作用域中的进程内 cron 库是最简单的触发器。croner 包是 Flue 自己的示例使用的:

src/app.ts
import { dispatch } from '@flue/runtime';
import { Cron } from 'croner';
import { Hono } from 'hono';
import { Reporter } from './agents/reporter.ts';

const app = new Hono();

new Cron(
  '0 9 * * *',
  {
    timezone: 'America/New_York',
    protect: true,
    catch: (error) => console.error('调度传递失败', error),
  },
  async () => {
    await dispatch(Reporter, {
      id: 'daily-summary',
      message: {
        kind: 'signal',
        type: 'schedule',
        body: '审查近期活动并准备每日摘要。',
        attributes: { scheduledAt: new Date().toISOString() },
      },
    });
  },
);

export default app;

Cron 实例在模块加载时创建,因此调度从服务器开始——生产中是 node dist/server.mjs,开发中是 vite dev。因为它也会在 vite dev 下触发,当不需要开发触发时,使用环境变量进行门控构造。进程内调度也在服务器的每个副本中运行:超过一个实例时,将触发器门控到单个副本或将其移动到平台调度器。

频率和时区属于 cron 库:croner 接受标准的五字段 cron 模式(带有可选的秒字段)、IANA timezone 选项、protect: true 在上一个回调仍在运行时跳过触发,以及回调错误的 catch 处理程序。任何进程内调度器都以相同方式工作——唯一与 Flue 相关的部分是 dispatch(...) 调用。

此模式的可运行版本在 examples/node-schedules 中提供。

在 Cloudflare 上调度

在 Cloudflare 目标上,Worker 不是长期运行的进程,因此平台拥有触发器。在 wrangler.jsonc 中声明频率为 Cron 触发器:

wrangler.jsonc
{
  "triggers": {
    "crons": ["0 9 * * *"],
  },
}

Cloudflare 以 UTC 评估 cron 表达式;没有时区选项。

触发作为 Worker scheduled 事件到达。从 src/cloudflare.ts 的默认导出贡献处理程序——Flue 将其合并到生成的 Worker 入口——并在其中调用 dispatch(...):

src/cloudflare.ts
import { dispatch } from '@flue/runtime';
import { Reporter } from './agents/reporter.ts';

export default {
  async scheduled(controller) {
    await dispatch(Reporter, {
      id: 'daily-summary',
      message: {
        kind: 'signal',
        type: 'schedule',
        body: '审查近期活动并准备每日摘要。',
        attributes: {
          cron: controller.cron,
          scheduledAt: new Date(controller.scheduledTime).toISOString(),
        },
      },
    });
  },
};

dispatch(...) 在 scheduled 处理程序中的工作方式与在 HTTP 路由中完全相同:它不需要挂载,绕过 HTTP 中间件,并在解析之前将消息持久地接收到智能体的 Durable Object 中。一个 Worker 有一个 scheduled 处理程序;当 crons 列出多个模式时,controller.cron 标识哪个触发了。

对于属于一个_现有_对话而不是应用程序的调度——运行中智能体的 Durable Object 内的后续计时器——Agents SDK 的 schedule()/scheduleEvery() API 可通过每模块的 extend() 扩展点使用。参见在 Cloudflare 上扩展智能体。这些回调与智能体执行共享对话的 Durable Object,因此在响应运行时到期的回调会在响应稳定后触发。当调度必须从外部寻址或创建对话时,Cron 触发器是正确的工具——它在 Worker 中运行,独立于任何对话的活动。

触发传递的内容

调度触发是一个结构化事件,因此将其作为 kind: 'signal' 消息传递:调用者定义的 type、body 中的指令,以及 attributes 中的扁平字符串元数据。信号在模型对话中渲染为 XML 标记的块:

<signal type="schedule" scheduledAt="2026-07-17T13:00:00.000Z">
审查近期活动并准备每日摘要。
</signal>

Hooks 和工具使用 useDelivery() 在代码中读取相同的传递,因此工具可以直接消费 attributes 值,而不依赖模型来回显它们。完整的消息形状,包括 tagName 覆盖,在 DeliveredMessage 中记录。

选择对话 ID

id 决定触发对智能体意味着什么:

ID 选择行为
固定('daily-summary')每次触发继续一个对话——消息累积,智能体在先前工作之上构建。
每次运行(crypto.randomUUID())每次触发是一个新的独立对话——智能体从零开始。
分区('weekly-report-' + currentWeekNumber())分区轮换——本周的所有触发进入同一个对话,新周开始新对话。

固定 ID 最常见。它维护一个随着每次调度而增长的持续对话历史,这对报告、仪表板或累积任务有意义。调度对话中不匹配任何传递的 kind('user'、'signal' 等)的普通消息被持久地接受——调度触发不取代用户输入。

当调度是自包含的并且不需要先前运行的上下文时,每次运行 ID 很有用:一次性数据导入、独立检查、无状态任务。每个触发都有自己的历史和状态。

分区 ID 提供了一种折衷方案——临时积累然后旋转。每周状态检查、每月报告或任何有自然重置窗口的任务都适合这里。智能体看到窗口内的一致历史,但看不到窗口之前的内容。

dispatch(...) 的 id 是字符串。当它与现有对话匹配时,消息进入其队列。当它不匹配时,会创建一个新的对话。没有显式的创建步骤——第一个 dispatch 调用隐式创建对话。

等待结果

dispatch(...) 在消息被持久接收时解析,而不是在智能体完成时。要等待实际完成,使用 dispatch(...) 返回的 submission 对象:

const { submission } = await dispatch(Reporter, {
  id: 'daily-summary',
  message: {
    kind: 'signal',
    type: 'schedule',
    body: '审查近期活动并准备每日摘要。',
  },
});

// 等待智能体完成
const result = await submission.wait();
console.log(result);

submission.wait() 返回一个 promise,当智能体完成响应时解析。它可以被中止:

const controller = new AbortController();

// 设置超时
setTimeout(() => controller.abort(), 30_000);

try {
  const result = await submission.wait({ signal: controller.signal });
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('调度超时');
  }
}

当触发处理器需要将结果持久化或在完成时发送通知时,等待结果很有用。对于触发并忘记的调度,只调用 dispatch(...) 就足够了。

错过触发和重叠

错过触发。 当服务器关闭或 Cloudflare Worker 未唤醒时,错过的触发不会回放。调度器是尽力而为的:如果触发在服务器关闭时到期,它就错过了。设计你的智能体来处理数据不连续性——它应该能够从上一个可用状态恢复,而不是假设每个触发都已执行。

重叠。 当一个触发在前一个触发仍在运行时执行,会发生重叠。行为取决于调度器:

  • Node.js + croner: protect: true 确保如果前一个回调仍在运行,则跳过触发。这是推荐的设置。
  • Cloudflare Cron 触发器: 如果先前的请求仍在运行,平台会传递新触发。如果调度是幂等的或如果先前的工作正在排队,这可能没问题;否则,使用智能体状态来门控并发执行。

对于严格要求顺序执行的调度,使用 protect: true(Node.js)或在智能体中实现应用级互斥锁(Cloudflare)。

持久性和可观测性

持久性。 调度的传递使用与用户消息相同的持久路径:dispatch(...) 在解析前将消息持久地接收到对话队列中。如果服务器在传递后但在智能体运行前崩溃,消息在重启时会被重新处理。智能体状态和对话历史是持久的,因此恢复的运行会从中断处继续。

可观测性。 调度的传递显示在对话历史中,就像任何其他消息一样。使用 useDelivery() hook 在代码中读取传递元数据——例如,attributes.scheduledAt 或 attributes.cron。对于日志记录,调度触发创建与用户消息相同类型的事件;过滤 kind: 'signal' 和 type: 'schedule' 以在对话流中标识它们。

模式和示例

每日摘要。 固定 ID,cron 0 9 * * *,累积历史。

每周报告。 分区 ID('weekly-report-' + currentWeekNumber()),cron 0 9 * * 1,每周从头开始。

心跳。 固定 ID,频繁 cron(*/5 * * * *),简单的单次响应状态检查。

数据同步。 每次运行 ID,cron 0 */6 * * *,独立的一次性数据导入。

链式调度。 一个调度触发另一个调度:第一个调度完成时,使用新消息调用 dispatch(...) 链接到下一步。

后续步骤