0x00 概要

OpenClaw 应该有40万行代码,阅读理解起来难度过大,因此,本系列通过Nanobot来学习 OpenClaw 的特色。

Nanobot是由香港大学数据科学实验室(HKUDS)开源的超轻量级个人 AI 助手框架,定位为"Ultra-Lightweight OpenClaw"。非常适合学习Agent架构。

Nanobot项目通过 CronService 和 CronTool 实现周期性任务:CronTool 作为面向 LLM 的接口,CronService 作为实际的业务逻辑执行者,两者通过依赖注入的方式协作,形成了一个完整的定时任务管理解决方案。

该方案通过三种模式覆盖:提醒、周期性任务、一次性任务的所有典型调度需求;通过 “简易参数 + 标准表达式” 的双轨时间配置,兼顾普通用户与专业用户的使用习惯;通过完整的任务生命周期管理与时区适配,解决了实际使用中的核心痛点。

该技能的落地,让 AI Agent 真正具备了 “时间驱动的主动执行能力”,是实现 Agent 从 “交互工具” 向 “自动化助手” 升级的关键一步,同时其轻量化的设计思路,也为 AI Agent 体系中其他能力组件的设计提供了参考 —— 贴合真实需求、降低使用门槛、实现能力闭环。

注:因为最近看的文章太多,所以如果有遗漏参考资料,还请读者指出,谢谢。

0x01 基本知识

1.1 需求

传统的定时任务(Cron Job)是僵化的,它只能在固定时间触发固定逻辑。随着大语言模型(LLM)的成熟,智能任务系统已成为AI应用的核心竞争点。这些任务的核心逻辑高度一致:将原本需要用户手动输入的提示词,转化为可在预定时间或周期内自动运行的工作流。这意味着即使用户处于离线状态,任务也会在云端自动执行,并在完成后通过通知系统将结果(如摘要、提醒)精准推送到用户面前。

如果将任务的触发时机由简单的“时间设定”扩展为由“事件变更”驱动,系统便进化为基于AI Agent的智能订阅任务。相比于传统模式,智能订阅任务具备三大核心优势:

  • 事件驱动(Event-Driven):不仅支持Cron表达式的时间触发,更支持外部数据(如油价波动、天气预警)的变化触发,灵活性显著提升。
  • 智能判断:利用大模型的语义理解能力,系统可以对复杂的触发条件进行逻辑判断,而非简单的阈值匹配。
  • 高度个性化:用户可以通过自然语言直接定义任务,系统自动解析并构建个性化的监测流。

1.2 问题

我们先思考下 AI Agent 的原生交互逻辑:常规情况下,Agent 仅能在用户发起明确指令后执行操作,属于 “被动响应” 模式,无法主动在指定时间触发动作,这就导致两类核心需求无法被满足

  • 一是个人场景中定点 / 周期性的提醒需求(如定时休息、会议提醒)
  • 二是工作场景中无需人工干预的周期性任务执行需求(如定时查询项目数据、每日同步信息)。

cron技能的核心定位,就是为 AI Agent 补上时间驱动的主动执行能力,将 Agent 的操作模式从 “用户指令触发” 拓展为 “时间触发 + 指令触发” 双模式。其本质是在 Agent 体系中嵌入了一套轻量、易用的任务调度引擎,通过标准化的参数配置,让用户无需掌握复杂的调度框架开发知识,即可快速实现提醒、任务的定时 / 周期性调度,同时支持任务的查询与删除,形成完整的调度能力闭环。

简单来说,cron技能解决的核心问题就是:让 AI Agent 能 “记着事、按时做”,摆脱对人工手动触发的依赖,实现轻量操作的自动化执行。传统的定时任务仅是“时间点”的触发,而基于AI Agent的智能任务则实现了逻辑上的质变。

1.3 价值

我们梳理上述所有设计与能力后,能总结出cron技能在 AI Agent 体系中的核心价值与落地意义,主要体现在三个层面:

  • 对用户:降低自动化操作门槛,提升效率。普通用户无需掌握编程、调度框架开发等技术,仅通过简单的参数配置,即可实现提醒与任务的自动化调度,将人从 “重复的手动触发操作” 中解放出来,节省时间与精力,同时完整的任务管理能力让使用更省心。
  • 对 Agent:丰富能力边界,提升实用价值。让 Agent 从 “被动响应的聊天工具” 升级为 “能主动做事的自动化助手”,突破了原生交互逻辑的限制,丰富了 Agent 的能力边界,让 Agent 不仅能解决 “即时问题”,还能解决 “未来的定时问题”,大幅提升了 Agent 的实际使用价值。
  • 对 AI Agent 体系:提供轻量调度方案,适配轻量化场景。相较于专业的分布式调度框架(如 Airflow、XXL-Job),cron 技能做了极致的轻量化设计,无需复杂的部署与配置,直接作为 Agent 的技能组件存在,完美适配了 AI Agent 场景下轻量、高频、简单的调度需求,为 Agent 体系提供了一套高适配性的轻量调度解决方案。

1.4 Claw0

1.4.1 架构

Claw0 的定时任务架构如下:

    Main Lane (user input):
        User Input --> lane_lock.acquire() -------> LLM --> Print
                       (blocking: always wins)

    Heartbeat Lane (background thread, 1s poll):
        should_run()?
            |no --> sleep 1s
            |yes
        _execute():
            lane_lock.acquire(blocking=False)
                |fail --> yield (user has priority)
                |success
            build prompt from HEARTBEAT.md + SOUL.md + MEMORY.md
                |
            run_agent_single_turn()
                |
            parse: "HEARTBEAT_OK"? --> suppress
                   meaningful text? --> duplicate? --> suppress
                                           |no
                                       output_queue.append()

    Cron Service (background thread, 1s tick):
        CRON.json --> load jobs --> tick() every 1s
            |
        for each job: enabled? --> due? --> _run_job()
            |
        error? --> consecutive_errors++ --> >=5? --> auto-disable
            |ok
        consecutive_errors = 0 --> log to cron-runs.jsonl

其要点如下:

  • Lane 互斥threading.Lock 在用户和心跳之间共享. 用户总是赢 (阻塞获取); 心跳让步 (非阻塞获取).
  • should_run(): 每次心跳尝试前的 4 个前置条件检查.
  • HEARTBEAT_OK: agent 用来表示"没有需要报告的内容"的约定.
  • CronService: 3 种调度类型 (ateverycron), 连续错误 5 次后自动禁用.
  • 输出队列: 后台结果通过线程安全的列表输送到 REPL.
1.4.2 CronService -- 3 种调度类型

任务定义在 CRON.json 中. 每个任务有一个 schedule.kind 和一个 payload:

@dataclass
class CronJob:
    id: str
    name: str
    enabled: bool
    schedule_kind: str       # "at" | "every" | "cron"
    schedule_config: dict
    payload: dict            # {"kind": "agent_turn", "message": "..."}
    consecutive_errors: int = 0

def _compute_next(self, job, now):
    if job.schedule_kind == "at":
        ts = datetime.fromisoformat(cfg.get("at", "")).timestamp()
        return ts if ts > now else 0.0
    if job.schedule_kind == "every":
        every = cfg.get("every_seconds", 3600)
        # 对齐到锚点, 保证触发时间可预测
        steps = int((now - anchor) / every) + 1
        return anchor + steps * every
    if job.schedule_kind == "cron":
        return croniter(expr, datetime.fromtimestamp(now)).get_next(datetime).timestamp()

连续 5 次错误后自动禁用:

if status == "error":
    job.consecutive_errors += 1
    if job.consecutive_errors >= 5:
        job.enabled = False
else:
    job.consecutive_errors = 0

1.5 ZeroClaw

我们再看看 ZeroClaw,进行比对。

下图是来自其官方文档的:“How the daemon keeps components alive”。从中看看 Cron 和 Heartbeat 的思路。

How the daemon keeps components alive

根据 ZeroClaw 的架构设计,这个流程图涵盖了以下核心逻辑:

  1. 组件并行启动
    • Daemon 启动后会立即并行生成四个核心部分:状态写入器(每5秒刷新)、网关渠道心跳调度器
    • 条件检查:渠道、心跳和调度器会根据配置文件(config.toml)中的设置决定是否启动对应的 Worker。例如,如果未配置 Cron,则直接标记为 OK 并跳过。
  2. 监督与循环
    • 每个核心组件(Gateway, Channels, Heartbeat, Scheduler)都拥有独立的 Supervisor(监督者) 和 Loop(循环)
    • 异常处理:如果组件意外退出或报错,系统会记录错误并进行退避等待(Backoff),随后尝试重新进入循环,确保服务的稳定性。
  3. 核心功能
    • Gateway:负责 HTTP/WebSocket 服务,处理外部连接。
    • Channels:连接 Telegram、Discord 等聊天平台。
    • Heartbeat定期执行后台感知任务,赋予 AI “自主意识”
    • Scheduler基于 Cron 表达式触发定时任务
  4. 优雅退出
    • 当接收到 Ctrl+C 信号时,Daemon 会中止所有任务并等待线程结束,确保数据完整保存后停止。

0x02 SKILL.md

该技能是实现 Agent定时任务调度与自动化提醒的核心能力组件,通过多模式调度、灵活的时间表达式配置及完整的任务生命周期管理,让 Agent 突破 “即时响应” 的交互限制,具备按指定时间 / 频率自动执行操作的能力,适配个人日常提醒、周期性轻量工作执行等典型业务场景,是提升 Agent 自动化能力与实用价值的关键模块。

cron技能的配置方式做了 **“简易参数 + 标准表达式” 双轨设计 **,同时支持时区适配,既降低了普通用户的使用门槛,又满足了专业用户的精细化调度需求,是其设计的核心亮点。

2.1 核心工作模式

该技能设计了三种核心工作模式,分别对应不同的业务需求,模式的划分遵循 “轻量到复杂、通知到执行” 的梯度设计,覆盖了绝大多数定时调度的典型场景。三种模式并非相互独立,而是可根据用户需求灵活选择,核心设计思路是 “按需匹配调度能力,让简单需求更轻量,让复杂需求更完整”

我们逐一拆解每种模式的核心逻辑、执行规则与适用场景:

2.1.1 Reminder(提醒模式)

前提背景:用户仅需要 Agent 在指定时间发送纯通知类信息,无需 Agent 执行任何额外的计算、查询、操作等动作,是最基础的调度需求。

核心逻辑:用户配置时间规则与提醒消息后,Agent 按规则触发时,直接将消息推送至用户,无任何后续执行步骤,触发后该次提醒任务完成(若为周期性配置,则循环触发)。

适用场景:个人日常定点 / 周期性提醒,如定时休息、喝水提醒、每日打卡提醒、会议提前通知等纯信息告知场景。

2.1.2 Task(任务模式)

前提背景:用户需要 Agent 不仅在指定时间触发动作,还需主动执行对应的业务操作,并将执行结果反馈给用户,属于 “调度 + 执行” 的复合需求,也是体现 Agent 自动化能力的核心模式。

核心逻辑:用户配置时间规则与任务描述消息后,Agent 按规则触发时,先解析消息中的任务指令、自主执行该任务(如查询 GitHub 星数、统计数据、同步文件等),再将执行结果整理后推送至用户,实现 “定时执行 + 结果反馈” 的一体化。

适用场景:周期性的轻量工作任务执行,如定时查询项目仓库数据、每日统计业务指标、定时检查服务状态并上报、每周同步文件等无需人工参与的自动化工作场景。

2.1.3 One-time(一次性模式)

前提背景:用户存在临时的定点调度需求,仅需 Agent 执行一次提醒 / 任务,无需重复触发,若使用常规周期性配置,还需手动删除任务,增加操作成本。

核心逻辑:用户配置单次触发的具体时间与消息(提醒 / 任务)后,Agent 在指定时间触发对应操作,执行完成后自动删除该调度任务,不会在 Agent 中留存冗余任务,无需用户手动干预。

适用场景:临时的单次提醒 / 任务,如某次临时会议的提醒、某个临时截止时间的任务执行、一次性的数据分析需求等非重复性的调度场景。

2.2 核心操作

cron技能设计了add/list/remove三个核心动作,覆盖了调度任务从创建→查询→删除的完整生命周期,避免出现 “任务创建后无法管理、冗余任务堆积” 的问题:

  • add:创建调度任务,搭配模式、时间、消息等参数,是核心动作;
  • list:查询当前已创建的所有调度任务,方便用户核对任务配置与状态;
  • remove:根据任务唯一标识job_id删除指定任务,支持精准清理无用任务。

该设计让cron技能不仅具备 “创建调度” 的基础能力,还拥有完整的任务管理能力,适配了实际使用中 “增删查” 的真实需求。

2.3 时间配置

时间配置是调度技能的核心,cron技能针对周期性调度设计了两种配置方式(简易参数与标准表达式),同时为一次性调度设计了专属的时间参数,完美匹配不同用户的配置习惯:

  1. 简易秒数参数(every_seconds)

    针对低频次、简单的周期性调度需求,用户可直接通过 every_seconds 来配置重复间隔的秒数,Agent 自动按该间隔循环触发,无需掌握任何调度表达式,比如 every_seconds=1200 即表示每 20 分钟触发一次。

    👉 适用:普通用户的简单周期性调度,配置成本为 0,易上手。

  2. 标准 cron 表达式(cron_expr)

    针对高频次、精细化的周期性调度需求,用户可通过标准的 5 位 cron 表达式配置时间规则,支持按 “分 / 时 / 日 / 月 / 周” 的精细化调度,比如 0 9 * * 1-5 即表示每周一至周五早上 9 点触发。

    👉 适用:专业用户的精细化调度,满足复杂的时间规则需求,兼容性强(符合行业通用标准)。

  3. 一次性时间参数(at)

    针对一次性调度需求,设计了专属的 at 参数,用户传入 ISO 标准的时间字符串,Agent 仅在该时间点触发一次,触发后自动删除任务,比如 at="" 即表示在指定的 ISO 时间执行。

同时,官方还做了自然语言→配置参数的映射表,将用户的自然语言表述(如 “每天 8 点”“工作日 5 点”)直接对应到具体的配置参数,进一步降低了用户的理解与配置成本,让非技术用户也能快速上手。

2.4 代码

---
name: cron
description: Schedule reminders and recurring tasks.
---

# Cron

Use the `cron` tool to schedule reminders or recurring tasks.

## Three Modes

1. **Reminder** - message is sent directly to user
2. **Task** - message is a task description, agent executes and sends result
3. **One-time** - runs once at a specific time, then auto-deletes

## Examples

Fixed reminder:
```
cron(action="add", message="Time to take a break!", every_seconds=1200)
```

Dynamic task (agent executes each time):
```
cron(action="add", message="Check HKUDS/nanobot GitHub stars and report", every_seconds=600)
```

One-time scheduled task (compute ISO datetime from current time):
```
cron(action="add", message="Remind me about the meeting", at="<ISO datetime>")
```

Timezone-aware cron:
```
cron(action="add", message="Morning standup", cron_expr="0 9 * * 1-5", tz="America/Vancouver")
```

List/remove:
```
cron(action="list")
cron(action="remove", job_id="abc123")
```

## Time Expressions

| User says | Parameters |
|-----------|------------|
| every 20 minutes | every_seconds: 1200 |
| every hour | every_seconds: 3600 |
| every day at 8am | cron_expr: "0 8 * * *" |
| weekdays at 5pm | cron_expr: "0 17 * * 1-5" |
| 9am Vancouver time daily | cron_expr: "0 9 * * *", tz: "America/Vancouver" |
| at a specific time | at: ISO datetime string (compute from current time) |

## Timezone

Use `tz` with `cron_expr` to schedule in a specific IANA timezone. Without `tz`, the server's local timezone is used.

0x03 核心组件

Cron功能的几个组件如下:

  • CronTool:负责接收用户指令
  • CronService:负责任务调度和生命周期管理
  • CronStore:负责持久化
  • CronJob + 子类:数据模型
  • AgentLoop:负责实际执行任务

3.1 依赖关系

类之间的持有关系如下:

  • AgentLoop → CronService → CronStore → CronJob
  • CronJob 由三个子数据类组成:CronSchedule, CronPayload, CronJobState
  • CronTool 持有 CronService 的引用

具体如下:

  • AgentLoop(AgentLoop ──▶ CronService):负责实际执行任务

    • 持有 CronService 的引用(组合关系,通过 cron_service 参数)
      • 创建时传入 cron_service 参数
      • 用于注册 CronTool 到工具列表
    • 在 _register_default_tools() 中创建 CronTool,传入 cron_service
    • 在 gateway 的 on_cron_job 回调中被调用(通过 process_direct())
  • CronService(CronService ──▶ CronStore)/(CronService.on_job (回调) ──▶ AgentLoop.process_direct):

    • 持有 CronStore(组合关系,内部存储,通过 _store),通过 _load_store() 加载
    • CronService 管理任务,CronStore 负责持久化
    • 持有 on_job 回调函数(类型是 Callable[[CronJob], Coroutine[Any, Any, str | None]]),on_job 回调由 Gateway 设置,调用 AgentLoop.process_direct()
    • 管理多个 CronJob(在 CronStore.jobs 列表中)
  • CronTool(CronTool ──▶ CronService ):

    • 持有 CronService 的引用(通过 _cron 成员变量),AgentLoop 创建 CronTool 时传入 CronService。
    • CronTool 通过 CronService 添加/查询任务
    • 持有 _channel 和 _chat_id(通过 set_context() 设置),用于保存用户上下文

更多推荐