这篇文档分析 Hermes Agent 如何管理和执行定期任务,重点回答两个容易误解的问题:
text
1. 模型到底怎么知道要调用 cronjob tool?
2. 模型调用 cronjob 时,参数怎么填?未来 cron run 使用的 prompt 从哪里来?
先给结论:
text
Hermes 的定期任务不是系统 crontab,也不是模型自己长期记住一个"每天执行"的意图。
它是 Hermes 自己的 cron 子系统:
1. 当前对话中的模型看到 cronjob tool 的完整 schema。
2. 模型根据 tool description 和参数说明,生成 cronjob tool call。
3. cronjob(action="create") 把 schedule、prompt、skills、script、deliver 等参数保存成 job。
4. 用户也可以通过 /cron、hermes cron CLI、Web/API 直接管理同一批 jobs。
5. job 持久化到 ~/.hermes/cron/jobs.json。
6. gateway 里的 cron ticker 默认每 60 秒调用 scheduler.tick()。
7. 到点后 scheduler 新建一个 fresh cron session,或在 no_agent 模式下只运行脚本。
8. scheduler 根据 job 记录拼出未来执行时的 prompt。
9. agent 执行后,scheduler 保存输出并统一投递。
10. 如果 B 任务依赖 A 任务结果,这个依赖不是自动推断的,而是 B 的 context_from 字段显式保存 A 的 job_id。
关键点是:创建任务时的模型和未来执行任务时的模型,看到的不是同一个 prompt。
text
创建任务时:
当前模型看到 cronjob tool schema。
它把未来任务指令写进 cronjob 的 prompt 参数。
未来执行时:
scheduler 从 jobs.json 读出 prompt 参数。
再拼接 cron 运行提示、script 输出、上游 job 输出、skill 内容。
然后把拼好的 prompt 发给一个新的 AIAgent session。
任务串联时:
A 任务先产生 output 文件。
B 任务的 context_from 保存 A 的 job_id。
B 运行前 scheduler 读取 A 最近一次 output,把它注入 B 的 prompt。
场景一:用户一句话创建"每天早上 9 点发 AI 日报"
用户说:
text
用户:每天早上 9 点帮我看一下 AI 新闻,整理成一份简短日报发给我。
1.1 模型不是靠系统 prompt 猜,而是看到 cronjob 工具
Hermes 并不是在代码里写死:
text
if 用户说"每天":
create_cron_job()
真正发生的是:
text
1. 当前会话加载了 cronjob toolset。
2. run_agent 把 cronjob 的 schema 作为 tool schema 暴露给模型。
3. 模型看到工具名、工具描述、参数描述。
4. 模型语义判断:用户要的是未来自动执行任务。
5. 模型调用 cronjob(action="create", ...)。
所以,模型知道能创建定期任务,主要来自 tool schema,而不是某段专门的 cron system prompt。
当然,Hermes 仍然会有通用的 tool calling 系统提示,告诉模型可以使用工具。但"cronjob 是什么、有哪些 action、prompt 怎么写、deliver 怎么填",这些 cron 专属知识来自 tools/cronjob_tools.py 里的 CRONJOB_SCHEMA。
1.2 cronjob 工具什么时候可用
工具注册时有一个 check_fn,决定当前环境是否允许使用 cronjob。
相关实现说明 / Availability
英文原文:
text
Available in interactive CLI mode and gateway/messaging platforms.
The cron system is internal (JSON file-based scheduler ticked by the gateway),
so no external crontab executable is required.
中文对照:
text
可用于交互式 CLI 模式和 gateway/messaging 平台。
cron 系统是 Hermes 内部系统,基于 JSON 文件存储,并由 gateway tick 调度。
所以不需要外部 crontab 可执行程序。
这段话说明:如果当前运行模式没有加载 cronjob 工具,模型就看不到它,也就不会调用它。能否调用取决于当前平台 toolset 和 check function。
1.3 直接管理入口不经过模型判断
除了自然语言触发 tool call,Hermes 还有几个直接管理入口:
bash
hermes cron create "0 9 * * *" "Check AI news and write a short daily briefing" --name "AI daily briefing"
hermes cron list
hermes cron status
hermes cron run <job_id>
hermes cron pause <job_id>
hermes cron resume <job_id>
hermes cron remove <job_id>
这些 CLI 命令不需要模型先判断"要不要调用 cronjob"。它们由 hermes_cli/cron.py 解析命令行参数,然后调用同一套底层 CRUD。
Hermes 还有 /cron slash command、Web API、gateway API。它们的入口不同,但最终都落到同一层:
text
自然语言 tool call
-> tools/cronjob_tools.py
-> cron/jobs.py
-> ~/.hermes/cron/jobs.json
/cron slash command
-> cli.py::_handle_cron_command()
-> tools/cronjob_tools.py
-> cron/jobs.py
hermes cron CLI
-> hermes_cli/cron.py
-> tools/cronjob_tools.py / cron.jobs
Web/API
-> hermes_cli/web_server.py 或 gateway/platforms/api_server.py
-> cron.jobs
这说明 Hermes 的"定期任务管理"不是模型专属能力。模型只是其中一个入口;真正的任务数据库和 scheduler 是 agent runtime 的基础设施。
模型可见的 cronjob Tool Schema:description 和 parameters 要一起读
这一节不是一个用户场景,而是模型调用 cronjob 时真正看到的"调用说明"。
Hermes 把 cronjob 暴露给模型时,不是只给一个工具名,也不是把参数藏在 system prompt 里。模型看到的是一份 tool schema:
text
工具名:
cronjob
工具描述:
description
参数结构:
parameters.properties
必填字段:
required
模型决定"要不要调用 cronjob",主要看 tool description。模型决定"怎么填 action、schedule、prompt、skills、script、deliver 等字段",主要看 parameters.properties。所以 description 和 parameters 必须放在一起理解,它们共同构成模型可见的 toolcall 说明。
Tool Description:模型为什么知道这是定时任务工具
相关 Tool Description / cronjob
英文原文:
text
Manage scheduled cron jobs with a single compressed tool.
Use action='create' to schedule a new job from a prompt or one or more skills.
Use action='list' to inspect jobs.
Use action='update', 'pause', 'resume', 'remove', or 'run' to manage an existing job.
To stop a job the user no longer wants: first action='list' to find the job_id, then action='remove' with that job_id. Never guess job IDs --- always list first.
Jobs run in a fresh session with no current-chat context, so prompts must be self-contained.
If skills are provided on create, the future cron run loads those skills in order, then follows the prompt as the task instruction.
On update, passing skills=[] clears attached skills.
NOTE: The agent's final response is auto-delivered to the target. Put the primary
user-facing content in the final response. Cron jobs run autonomously with no user
present --- they cannot ask questions or request clarification.
Important safety rule: cron-run sessions should not recursively schedule more cron jobs.
中文对照:
text
用一个压缩后的统一工具管理 scheduled cron jobs。
使用 action='create' 从 prompt 或一个/多个 skills 创建新任务。
使用 action='list' 查看任务。
使用 action='update'、'pause'、'resume'、'remove' 或 'run' 管理已有任务。
如果要停止用户不再需要的任务:先 action='list' 找到 job_id,再 action='remove' 携带该 job_id。不要猜 job ID,必须先 list。
任务会在一个全新 session 中运行,没有当前聊天上下文,所以 prompt 必须自包含。
如果 create 时提供了 skills,未来 cron run 会按顺序加载这些 skills,然后把 prompt 作为任务指令执行。
update 时传 skills=[] 会清空绑定的 skills。
注意:agent 的最终回复会自动投递到目标位置。主要面向用户的内容要放在最终回复里。
cron jobs 会无人值守运行,没有用户在线,所以不能提问或请求澄清。
重要安全规则:cron-run sessions 不应该递归创建更多 cron jobs。
这段描述回答的是"什么时候该用这个工具":
text
用户要创建未来任务
-> action='create'
用户要查看任务
-> action='list'
用户要改、停、恢复、删除、立即运行已有任务
-> action='update' / 'pause' / 'resume' / 'remove' / 'run'
用户要停止某个任务
-> 先 list 找 job_id,再 remove,不能猜 ID
未来任务无人值守运行
-> prompt 必须自包含,不能依赖当前聊天上下文
未来 cron agent 的最终回复会自动投递
-> prompt 里应该要求模型把主要内容放在 final response,而不是自己 send_message
Parameters:模型如何填 tool call
cronjob 的 tool schema 只有一个 JSON 层面的必填参数:
text
required: ["action"]
但是不同 action 会要求不同字段。例如:
text
create:
需要 schedule。
默认 no_agent=False 时,需要 prompt 或 skills 至少一个。
no_agent=True 时,需要 script。
update / pause / resume / remove / run:
需要 job_id。
这些更细的要求不是 JSON schema 的 required 一次性表达完的,而是在 handler 逻辑里校验。
下面是模型可见的主要参数说明。每个参数的英文原文来自 CRONJOB_SCHEMA["parameters"]["properties"];中文对照用于解释它如何影响模型生成 tool call。
action:先判断这是 create 还是管理已有任务
相关 Tool Parameter / action
英文原文:
text
One of: create, list, update, pause, resume, remove, run
中文对照:
text
取值之一:create、list、update、pause、resume、remove、run
这告诉模型:cronjob 是一个 action-style 工具,而不是每种操作一个工具。
创建新任务时,模型填:
json
{
"action": "create"
}
如果用户说的是"列一下现有定时任务",才会填:
json
{
"action": "list"
}
如果用户说的是"删除那个任务",则不能直接猜 job_id,要先 list,再 remove:
json
{
"action": "remove",
"job_id": "a1b2c3d4e5f6"
}
job_id:管理已有任务时才需要
相关 Tool Parameter / job_id
英文原文:
text
Required for update/pause/resume/remove/run
中文对照:
text
update / pause / resume / remove / run 时必需。
这就是为什么工具描述里强调"不要猜 job ID"。当用户说:
text
用户:停掉那个 AI 日报。
模型应该先:
json
{
"action": "list"
}
拿到 job_id 后再:
json
{
"action": "remove",
"job_id": "a1b2c3d4e5f6"
}
创建新任务时,job_id 不需要由模型提供。create_job() 会生成新的 job_id。
prompt:把未来任务写成自包含指令
这是最关键的参数。
相关 Tool Parameter / prompt
英文原文:
text
For create: the full self-contained prompt. If skills are also provided, this becomes the task instruction paired with those skills.
中文对照:
text
create 时使用:完整、自包含的 prompt。
如果同时提供了 skills,这个 prompt 会成为与这些 skills 搭配使用的任务指令。
这段参数说明回答了你问的核心问题:后续 cron run 要用的用户任务指令,主要来自 prompt 参数,不是另一个隐藏 system prompt。
例如用户说:
text
用户:每天早上 9 点帮我看一下 AI 新闻,整理成一份简短日报发给我。
模型不应该把 prompt 写成:
text
看一下新闻。
因为未来任务没有当前聊天上下文。它应该写成自包含指令:
text
Check recent AI news from reliable sources and produce a concise Chinese daily briefing for the user. Include 3-5 important items, why they matter, and source links when available. If there is no meaningful new information, respond with [SILENT].
这个字符串会保存在 job 的 prompt 字段里。未来到点后,scheduler 会把它作为执行 prompt 的主体。
注意:prompt 应该只写未来运行时要完成的任务内容。调度时间应该放进 schedule,投递目标应该交给 deliver/origin,不要把所有信息都塞进 prompt 让 scheduler 再猜。
schedule:把自然语言时间变成机器可执行时间
相关 Tool Parameter / schedule
英文原文:
text
For create/update: '30m', 'every 2h', '0 9 * * *', or ISO timestamp
中文对照:
text
create / update 时使用:可以是 '30m'、'every 2h'、'0 9 * * *' 或 ISO timestamp。
模型看到这个参数说明后,知道可以这样填:
json
{
"schedule": "0 9 * * *"
}
Hermes 的 parse_schedule() 会把它解析成:
json
{
"kind": "cron",
"expr": "0 9 * * *",
"display": "0 9 * * *"
}
支持格式:
text
30m / 2h / 1d
一次性延迟任务。
every 30m / every 2h
固定间隔重复任务。
0 9 * * *
cron 表达式。
2026-05-20T09:00:00
ISO 时间戳,一次性任务。
"每天上午 10 点"可以对应:
json
{
"schedule": "0 10 * * *"
}
name:给任务一个方便管理的名字
相关 Tool Parameter / name
英文原文:
text
Optional human-friendly name
中文对照:
text
可选的人类友好名称。
这个字段不是执行逻辑必需,但对管理很重要。用户后续说"停掉 AI 日报",模型可以通过 list 看到 name,更容易匹配正确 job。
示例:
json
{
"name": "App health daily check"
}
repeat:控制任务最多执行多少次
相关 Tool Parameter / repeat
英文原文:
text
Optional repeat count. Omit for defaults (once for one-shot, forever for recurring).
中文对照:
text
可选重复次数。省略时使用默认值:一次性任务默认执行一次,周期任务默认永久执行。
比如用户说:
text
用户:接下来三天每天早上提醒我提交日报。
模型可以填:
json
{
"schedule": "0 9 * * *",
"repeat": 3
}
执行三次后,mark_job_run() 会让任务结束。
"连续跑 7 天"可以对应:
json
{
"repeat": 7
}
deliver:控制未来输出投递到哪里
相关 Tool Parameter / deliver
英文原文:
text
Omit this parameter to auto-deliver back to the current chat and topic (recommended). Auto-detection preserves thread/topic context. Only set explicitly when the user asks to deliver somewhere OTHER than the current conversation. Values: 'origin' (same as omitting), 'local' (no delivery, save only), or platform:chat_id:thread_id for a specific destination. Examples: 'telegram:-1001234567890:17585', 'discord:#engineering', 'sms:+15551234567'. WARNING: 'platform:chat_id' without :thread_id loses topic targeting.
中文对照:
text
建议省略这个参数,以自动投递回当前 chat 和 topic。自动检测会保留 thread/topic 上下文。
只有当用户明确要求投递到当前对话以外的位置时,才显式设置。
取值包括:'origin'(等同于省略)、'local'(不投递,只保存)、或 platform:chat_id:thread_id 形式的具体目标。
示例:'telegram:-1001234567890:17585'、'discord:#engineering'、'sms:+15551234567'。
警告:'platform:chat_id' 如果没有 :thread_id,会丢失 topic 定位。
这个参数说明很重要:模型通常不应该主动填 deliver。如果用户是在 Telegram topic 中创建任务,省略 deliver 反而能保留 topic。
示例:
text
用户:每天 9 点把日报发到当前群。
推荐 tool call:
json
{
"action": "create",
"schedule": "0 9 * * *",
"prompt": "Produce a concise daily briefing...",
"name": "Daily briefing"
}
不需要显式填:
json
{
"deliver": "telegram:-1001234567890"
}
否则可能丢失 thread/topic。
如果用户说"发到当前群"或"发给我",模型通常应该省略 deliver。工具 handler 会通过 _origin_from_env() 保存当前平台、chat_id 和 thread_id,create_job() 默认把 deliver 设为 origin。
skills:把指定 skill 保存进 job
相关 Tool Parameter / skills
英文原文:
text
Optional ordered list of skill names to load before executing the cron prompt. On update, pass an empty array to clear attached skills.
中文对照:
text
可选的有序 skill 名称列表。执行 cron prompt 前会先加载这些 skills。
update 时传空数组可以清空已绑定 skills。
如果用户说:
text
用户:每天用"技术文章摘要"这个 skill 给我写 AI 工程日报。
模型可以调用:
json
{
"action": "create",
"schedule": "0 8 * * *",
"skills": ["technical-article-digest"],
"prompt": "Collect AI engineering articles from the last 24 hours and produce a concise Chinese digest for developers.",
"name": "AI engineering digest"
}
这里的 skills 不是让未来模型自己去 skills_list 里挑。它会被保存进 job,未来执行时 scheduler 直接 skill_view(skill_name) 加载指定 skill 内容。
model:为这个任务固定模型
相关 Tool Parameter / model
英文原文:
text
Optional per-job model override. If provider is omitted, the current main provider is pinned at creation time so the job stays stable.
中文对照:
text
可选的 job 级模型覆盖。如果省略 provider,会在创建时固定当前主 provider,让这个 job 后续保持稳定。
嵌套字段:
相关 Tool Parameter / model.provider
英文原文:
text
Provider name (e.g. 'openrouter', 'anthropic', or 'custom:<name>' for a provider defined in custom_providers config --- always include the ':<name>' suffix, never pass the bare 'custom'). Omit to use and pin the current provider.
中文对照:
text
Provider 名称,例如 'openrouter'、'anthropic',或者 custom_providers 配置中的 'custom:<name>'。
必须包含 ':<name>' 后缀,不要传裸的 'custom'。
省略时使用并固定当前 provider。
相关 Tool Parameter / model.model
英文原文:
text
Model name (e.g. 'anthropic/claude-sonnet-4', 'claude-sonnet-4')
中文对照:
text
模型名称,例如 'anthropic/claude-sonnet-4'、'claude-sonnet-4'。
示例:
json
{
"model": {
"provider": "anthropic",
"model": "claude-sonnet-4"
}
}
如果用户只说"用 Claude Sonnet",模型要根据当前 provider 配置和可用模型名选择可解析的 provider/model。若 provider 省略,Hermes 会在创建时 pin 当前主 provider,防止未来默认模型变化导致 job 漂移。
script:先采集数据,再让模型分析
相关 Tool Parameter / script
英文原文:
text
Optional path to a script that runs each tick. In the default mode its stdout is injected into the agent's prompt as context (data-collection / change-detection pattern). With no_agent=True, the script IS the job and its stdout is delivered verbatim (classic watchdog pattern). Relative paths resolve under {display_hermes_home()}/scripts/. ``.sh``/``.bash`` extensions run via bash, everything else via Python. On update, pass empty string to clear.
中文对照:
text
可选脚本路径,每次 tick 都会运行。
默认模式下,脚本 stdout 会作为上下文注入 agent prompt,用于数据采集/变化检测。
当 no_agent=True 时,脚本本身就是任务,它的 stdout 会原样投递,这是经典 watchdog 模式。
相对路径会解析到 {display_hermes_home()}/scripts/。
`.sh` / `.bash` 扩展名用 bash 运行,其他文件用 Python 运行。
update 时传空字符串可以清空。
这里的 {display_hermes_home()} 是源码中的 f-string 表达,运行时会变成当前 Hermes home,通常类似:
text
~/.hermes/scripts/
脚本路径不会任意执行系统路径,Hermes 会在 API 边界和执行时都校验路径必须留在 scripts 目录内。
例如模型填 "script": "check_server.py" 时,它指的是相对 ~/.hermes/scripts/ 的脚本路径,不是项目目录里的 /home/tlinux/app/check_server.py。如果脚本需要在项目目录里工作,由 workdir 控制执行上下文。
no_agent:什么时候跳过 LLM
相关 Tool Parameter / no_agent
英文原文:
text
Default: False (LLM-driven job --- the agent runs the prompt each tick). Set True to skip the LLM entirely: the scheduler just runs ``script`` on schedule and delivers its stdout verbatim. No tokens, no agent loop, no model override honoured.
REQUIREMENTS when True: ``script`` MUST be set (``prompt`` and ``skills`` are ignored).
DELIVERY SEMANTICS when True: (a) non-empty stdout is sent verbatim as the message; (b) EMPTY stdout means SILENT --- nothing is sent to the user and they won't see anything happened, so design your script to stay quiet when there's nothing to report (the watchdog pattern); (c) non-zero exit / timeout sends an error alert so a broken watchdog can't fail silently.
WHEN TO USE True: recurring script-only pings where the script itself produces the exact message text (memory/disk/GPU watchdogs, threshold alerts, heartbeats, CI notifications, API pollers with a fixed output shape). WHEN TO USE False (default): anything that needs reasoning --- summarize a feed, draft a daily briefing, pick interesting items, rephrase data for a human, follow conditional logic based on content.
中文对照:
text
默认值 False,表示 LLM 驱动任务,每次 tick 都由 agent 运行 prompt。
设为 True 会完全跳过 LLM:scheduler 只按计划运行 script,并原样投递 stdout。
不消耗 tokens,不进入 agent loop,也不使用 model override。
当 True 时的要求:必须设置 script,prompt 和 skills 会被忽略。
当 True 时的投递语义:
(a) 非空 stdout 会作为消息原样发送;
(b) 空 stdout 表示静默,不向用户发送任何内容,所以脚本应该在无事发生时保持安静;
(c) 非零退出或超时会发送错误告警,避免 watchdog 坏掉后静默失败。
什么时候用 True:
适合周期性脚本 ping,脚本自己能生成准确消息,比如内存/磁盘/GPU watchdog、阈值告警、心跳、CI 通知、固定输出格式的 API poller。
什么时候用 False:
需要推理的任务,比如总结 feed、写日报、挑选重要条目、把数据改写成人类可读报告、根据内容做条件判断。
如果任务需要按 skill 总结、筛选、改写、判断重要性,就需要 LLM。模型不应该设置:
json
{
"no_agent": true
}
默认 no_agent=False 即可。这样脚本 stdout 会注入 prompt,模型再根据 prompt 和 skills 生成最终报告。
context_from:显式串联上游 job 输出
相关 Tool Parameter / context_from
英文原文:
text
Optional job ID or list of job IDs whose most recent completed output is injected into the prompt as context before each run. Use this to chain cron jobs: job A collects data, job B processes it. Each entry must be a valid job ID (from cronjob action='list'). Note: injects the most recent completed output --- does not wait for upstream jobs running in the same tick. On update, pass an empty array to clear.
中文对照:
text
可选 job ID 或 job ID 列表。每次运行前,会把这些 job 最近一次完成的输出注入 prompt 作为上下文。
用于串联 cron jobs:Job A 采集数据,Job B 处理数据。
每个条目都必须是有效 job ID,可以通过 cronjob action='list' 获取。
注意:它注入的是最近一次已完成输出,不会等待同一个 tick 中正在运行的上游 job。
update 时传空数组可以清空。
这个字段同样要求模型不要猜 ID。要先 list,再把真实 job_id 填进去。
没有"读取另一个 cron job 输出"的需求时,不填 context_from。后面的"任务串联"场景会专门讲 B 任务如何依赖 A 任务。
enabled_toolsets:限制未来 cron agent 的工具面
相关 Tool Parameter / enabled_toolsets
英文原文:
text
Optional list of toolset names to restrict the job's agent to (e.g. ["web", "terminal", "file", "delegation"]). When set, only tools from these toolsets are loaded, significantly reducing input token overhead. When omitted, all default tools are loaded. Infer from the job's prompt --- e.g. use "web" if it calls web_search, "terminal" if it runs scripts, "file" if it reads files, "delegation" if it calls delegate_task. On update, pass an empty array to clear.
中文对照:
text
可选 toolset 名称列表,用来限制这个 job 的 agent 可用工具,比如 ["web", "terminal", "file", "delegation"]。
设置后,只加载这些 toolsets 中的工具,可以显著减少输入 token 开销。
省略时,加载默认工具。
应根据 job prompt 推断,例如需要 web_search 就用 "web",需要运行脚本就用 "terminal",需要读文件就用 "file",需要 delegate_task 就用 "delegation"。
update 时传空数组可以清空。
这告诉模型:创建长期任务时,可以主动缩小未来执行的工具面,减少每次 cron run 的 tool schema token。
如果用户明确说"只给 terminal 和 file 工具",模型可以填:
json
{
"enabled_toolsets": ["terminal", "file"]
}
workdir:让任务在项目目录上下文中运行
相关 Tool Parameter / workdir
英文原文:
text
Optional absolute path to run the job from. When set, AGENTS.md / CLAUDE.md / .cursorrules from that directory are injected into the system prompt, and the terminal/file/code_exec tools use it as their working directory --- useful for running a job inside a specific project repo. Must be an absolute path that exists. When unset (default), preserves the original behaviour: no project context files, tools use the scheduler's cwd. On update, pass an empty string to clear. Jobs with workdir run sequentially (not parallel) to keep per-job directories isolated.
中文对照:
text
可选绝对路径,表示从这个目录运行 job。
设置后,该目录下的 AGENTS.md / CLAUDE.md / .cursorrules 会注入 system prompt,terminal/file/code_exec 工具也会使用它作为工作目录。
适合在具体项目仓库里运行任务。
必须是已存在的绝对路径。
未设置时保持原行为:不注入项目上下文文件,工具使用 scheduler 的 cwd。
update 时传空字符串可以清空。
带 workdir 的 jobs 会顺序执行,而不是并行执行,以保持每个 job 的目录隔离。
如果任务要在项目目录 /home/tlinux/app 内运行,模型可以填:
json
{
"workdir": "/home/tlinux/app"
}
这会影响两件事:
text
1. AGENTS.md / CLAUDE.md / .cursorrules 等项目上下文会注入 system prompt。
2. terminal/file/code_exec 工具会以这个目录作为工作目录。
这就是模型调用 cronjob 时的完整可见说明面:description 告诉模型这个工具负责什么,parameters 告诉模型每个字段怎么填,handler 再在运行时做更严格的业务校验。
场景二:模型如何把用户请求转换成 tool call
用户说:
text
用户:每天早上 9 点帮我看一下 AI 新闻,整理成一份简短日报发给我。
模型看到 tool schema 后,通常会做这样的语义拆解:
text
每天早上 9 点
-> schedule = "0 9 * * *"
看 AI 新闻,整理简短日报
-> prompt = 完整自包含任务指令
发给我
-> deliver 省略,让 Hermes 自动投递回当前 chat/topic
任务名称
-> name = "AI daily briefing"
合理的 tool call 是:
json
{
"action": "create",
"schedule": "0 9 * * *",
"prompt": "Check recent AI news from reliable sources and produce a concise Chinese daily briefing for the user. Include 3-5 important items, why they matter, and source links when available. If there is no meaningful new information, respond with [SILENT].",
"name": "AI daily briefing"
}
注意这里的 prompt 必须是"未来可独立执行"的指令,而不是引用当前对话:
text
不好:
"按刚才说的方式做日报。"
好:
"Check recent AI news from reliable sources and produce a concise Chinese daily briefing..."
这正是 prompt 参数描述里 "full self-contained prompt" 的含义。
场景三:用户提出复杂定时任务,模型按 schema 组装参数
用户说:
text
用户:从明天开始,每天上午 10 点检查 /home/tlinux/app 的测试和服务健康状态。
先运行 check_server.py 采集数据,如果有异常,就按 incident-daily-report 这个 skill 总结原因并发到当前群。
只需要连续跑 7 天。这个任务只给它 terminal 和 file 工具就够了,用 Claude Sonnet 跑。
这才是一个真实场景:用户给的是自然语言目标,模型需要按照前面那份 cronjob tool schema 生成结构化参数。
模型看到这句话后,不是先执行检查,而是先判断"这是未来自动任务":
text
"从明天开始,每天上午 10 点"
-> action="create"
-> schedule="0 10 * * *"
"检查 /home/tlinux/app"
-> workdir="/home/tlinux/app"
"先运行 check_server.py 采集数据"
-> script="check_server.py"
-> 但仍需要模型总结,所以 no_agent 不填,保持默认 False
"按 incident-daily-report 这个 skill 总结原因"
-> skills=["incident-daily-report"]
"发到当前群"
-> deliver 省略,让 origin 自动保存当前 chat/topic
"连续跑 7 天"
-> repeat=7
"只给 terminal 和 file 工具"
-> enabled_toolsets=["terminal", "file"]
"用 Claude Sonnet 跑"
-> model={...}
这里最容易出错的是 prompt。模型不应该把用户原话整段塞进去,也不应该写成"按上面要求检查"。因为未来 cron run 是 fresh session,没有当前聊天上下文。合理的 prompt 应该只保留未来执行时需要的自包含任务指令:
text
Use the pre-run script output to inspect the health and test status of /home/tlinux/app. If everything is healthy and there is no actionable issue, respond exactly with [SILENT]. If there are failures or health risks, summarize the failing checks, likely causes, relevant files or commands, and recommended next actions in Chinese.
注意:prompt 不需要包含"每天上午 10 点",因为调度时间已经放进 schedule;也不需要包含"发到当前群",因为投递由 deliver/origin 处理;也不需要重复写"使用 incident-daily-report skill",因为 skills 字段会保存这个关系。
最终 tool call 类似:
json
{
"action": "create",
"schedule": "0 10 * * *",
"name": "App health daily check",
"repeat": 7,
"skills": ["incident-daily-report"],
"script": "check_server.py",
"prompt": "Use the pre-run script output to inspect the health and test status of /home/tlinux/app. If everything is healthy and there is no actionable issue, respond exactly with [SILENT]. If there are failures or health risks, summarize the failing checks, likely causes, relevant files or commands, and recommended next actions in Chinese.",
"workdir": "/home/tlinux/app",
"enabled_toolsets": ["terminal", "file"],
"model": {
"provider": "anthropic",
"model": "claude-sonnet-4"
}
}
这里没有 deliver,因为"当前群"由 origin 自动保存;没有 context_from,因为这个任务没有上游 cron output 依赖;没有 no_agent,因为默认 LLM 模式正好符合"脚本采集 + skill 总结"。
场景四:创建成功后,job 里到底保存了什么
cronjob(action="create") 最终会调用 create_job(),写入:
text
~/.hermes/cron/jobs.json
典型结构:
json
{
"id": "a1b2c3d4e5f6",
"name": "AI daily briefing",
"prompt": "Check recent AI news from reliable sources and produce a concise Chinese daily briefing...",
"skills": [],
"skill": null,
"model": null,
"provider": null,
"base_url": null,
"script": null,
"no_agent": false,
"context_from": null,
"schedule": {
"kind": "cron",
"expr": "0 9 * * *",
"display": "0 9 * * *"
},
"schedule_display": "0 9 * * *",
"repeat": {
"times": null,
"completed": 0
},
"enabled": true,
"state": "scheduled",
"created_at": "2026-05-19T10:00:00+08:00",
"next_run_at": "2026-05-20T09:00:00+08:00",
"last_run_at": null,
"last_status": null,
"last_error": null,
"last_delivery_error": null,
"deliver": "origin",
"origin": {
"platform": "telegram",
"chat_id": "-1001234567890",
"thread_id": "17585"
},
"enabled_toolsets": null,
"workdir": null
}
这里最重要的是:
text
prompt
未来执行的任务主体。
schedule
已解析的触发规则。
next_run_at
下一次触发时间。
origin / deliver
未来投递位置。
skills / script / context_from / workdir / enabled_toolsets
未来执行 prompt 时的上下文和运行环境。
场景五:未来到点后,runtime prompt 是怎么拼出来的
这是另一个关键问题:未来 cron run 使用的 prompt,不只是 job 里的 prompt 原样发送。
默认 LLM 模式下,cron/scheduler.py::_build_job_prompt() 会按顺序构造最终 prompt。
5.1 拼接顺序
实际顺序是:
text
1. 从 job["prompt"] 取出基础任务指令。
2. 如果配置 script,先运行 script:
- 成功且有 stdout:把 Script Output 插到 prompt 前面。
- 失败:把 Script Error 插到 prompt 前面。
- 成功但 stdout 为空:返回 None,跳过 AI call。
- stdout 最后一行 {"wakeAgent": false}:跳过 AI call。
3. 如果配置 context_from:
- 读取上游 job 最近一次 output。
- 截断到 8000 字符。
- 插到 prompt 前面。
4. 给 prompt 最前面加 cron execution guidance。
5. 如果配置 skills:
- 读取每个 skill 的 SKILL.md。
- 把 skill 内容放到 prompt 前面。
- 最后追加 "The user has provided..." 包住前面拼好的 cron prompt。
6. 调用 AIAgent.run_conversation(prompt)。
也就是说,未来执行的 prompt 来源分成两类:
text
用户/模型创建时写入:
job["prompt"]
scheduler 运行时追加:
cron execution guidance
script output / script error
context_from output
skill content
missing skill notice
5.2 cron execution guidance
每个 LLM cron job 都会注入下面这段提示词。
相关提示词 / Cron Execution Guidance
英文原文:
text
[IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user --- do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly "[SILENT]" (nothing else) to suppress delivery. Never combine [SILENT] with content --- either report your findings normally, or say [SILENT] and nothing more.]
中文对照:
text
[重要:你正在作为一个 scheduled cron job 运行。
投递:你的最终回复会自动发送给用户,不要使用 send_message,也不要尝试自己投递输出。
只需要把报告/输出作为最终回复生成,系统会处理剩下的事情。
静默:如果确实没有任何新内容需要报告,请只回复 "[SILENT]",不要包含其他内容,以抑制投递。
不要把 [SILENT] 和正文混在一起。要么正常报告发现,要么只说 [SILENT]。]
这段不是创建时 tool schema 的一部分,而是未来执行时 scheduler 追加到 prompt 里的运行提示。
它控制未来 agent 的行为:
text
1. 不要自己调用 send_message。
2. 最终回复会被 scheduler 自动投递。
3. 没有新内容时可以用 [SILENT] 抑制投递。
5.3 script output 注入
如果 job 配置了 script,默认模式下脚本 stdout 会被注入到 prompt 里。
相关提示词 / Script Output Injection
英文原文:
text
## Script Output
The following data was collected by a pre-run script. Use it as context for your analysis.
```
{script_output}
```
{prompt}
中文对照:
text
## 脚本输出
下面的数据由预运行脚本采集。请把它作为分析上下文。
```
{script_output}
```
{prompt}
这里的 {prompt} 是 job 里原来的任务指令。script output 会插到它前面。
如果脚本失败,会注入错误:
相关提示词 / Script Error Injection
英文原文:
text
## Script Error
The data-collection script failed. Report this to the user.
```
{script_output}
```
{prompt}
中文对照:
text
## 脚本错误
数据采集脚本失败。请把这个情况报告给用户。
```
{script_output}
```
{prompt}
这说明 script 不是另一个 tool call,也不是模型主动调用的工具。它是 scheduler 在唤醒 agent 之前运行的预处理步骤。
5.4 context_from 注入:B 任务如何显式依赖 A 任务
context_from 是 Hermes cron 里最容易误解的字段。
它不是自动依赖推断,也不是 "B 任务看到 A 任务名字所以自动读取 A"。Hermes 不会分析两个 prompt 之间的语义关系来判断依赖。
真正机制是:
text
A 任务创建后得到 job_id。
B 任务创建或更新时,把 A 的 job_id 写进 context_from。
B 每次运行前,scheduler 根据 context_from 读取 A 最近一次 completed output。
读取到的 output 被注入到 B 的 prompt 前面。
也就是说,B 任务依赖 A 任务,是通过 B job record 里的 context_from 字段显式表达的。
创建 A:采集数据任务
用户可以先创建 A:
text
用户:每小时抓取一次竞品 release notes,只保存本地,不需要发给我。
模型可能调用:
json
{
"action": "create",
"schedule": "every 1h",
"name": "competitor release collector",
"prompt": "Fetch recent competitor release notes and produce a concise raw change list with source links. Focus on product, pricing, API, and enterprise feature changes.",
"deliver": "local",
"enabled_toolsets": ["web"]
}
创建成功后,工具返回里会有:
json
{
"success": true,
"job_id": "111aaa222bbb",
"name": "competitor release collector"
}
A 每次运行后会保存输出:
text
~/.hermes/cron/output/111aaa222bbb/<timestamp>.md
创建 B:读取 A 输出并汇总
然后用户说:
text
用户:每天 17 点把刚才那个竞品采集任务的结果整理成中文摘要发给我。
模型不能只在 prompt 里写"读取刚才那个任务"。它必须把 A 的 job_id 放到 B 的 context_from。
如果模型已经知道 A 的 job_id,可以直接创建 B:
json
{
"action": "create",
"schedule": "0 17 * * *",
"name": "competitor daily digest",
"context_from": ["111aaa222bbb"],
"prompt": "Using the upstream competitor release collector output, write a concise Chinese daily digest. Group changes by competitor, highlight business impact, and include source links. If the upstream output contains no meaningful changes, respond exactly with [SILENT].",
"enabled_toolsets": ["web"]
}
如果模型不知道 A 的 job_id,必须先:
json
{
"action": "list"
}
从返回的 jobs 里找到 competitor release collector,再创建或 update B。
B 运行时怎么读 A
当 B 到点执行时,scheduler 会:
text
1. 读取 B job 的 context_from=["111aaa222bbb"]。
2. 校验 job_id 只包含 12 位 hex 字符,避免路径穿越。
3. 打开 ~/.hermes/cron/output/111aaa222bbb/。
4. 按 mtime 找最近的 .md 输出文件。
5. 读取内容并截断到 8000 字符。
6. 把内容注入 B 的 prompt 前面。
注入模板是:
相关提示词 / Output From Previous Job
英文原文:
text
## Output from job '{source_job_id}'
The following is the most recent output from a preceding cron job. Use it as context for your analysis.
```
{latest_output}
```
{prompt}
中文对照:
text
## 来自 job '{source_job_id}' 的输出
下面是前一个 cron job 最近一次输出。请把它作为分析上下文。
```
{latest_output}
```
{prompt}
这不是模型自己搜索历史,也不是 session_search。它是 scheduler 从:
text
~/.hermes/cron/output/<source_job_id>/*.md
读取最近一个输出文件,最多注入 8000 字符。
context_from 的边界
这个设计很轻量,但它不是完整 DAG scheduler。
text
不会等待:
如果 A 和 B 在同一个 tick 同时到期,B 不会等待 A 本轮执行完成。
B 读到的是 A 最近一次已经完成的 output。
不会自动推断:
B 不会因为 prompt 里写了"读取竞品采集任务"就自动找到 A。
必须通过 context_from 保存 A 的 job_id。
不会读取全部历史:
只读最近一次 output,不读 A 的所有输出。
会截断:
最近输出超过 8000 字符时会截断,防止 B 的 prompt 爆炸。
如果你需要强 DAG 语义,比如 "A 完成立刻触发 B,并把 A 本轮完整 artifact 传给 B",Hermes 当前 cron 的 context_from 不是这个模型。它更像"定时读取上游最近产物"。
5.5 skill 注入
如果 job 保存了 skills,scheduler 会读取 skill 内容并拼进 prompt。
相关提示词 / Skill Injection In Cron
英文原文:
text
[IMPORTANT: The user has invoked the "{skill_name}" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]
{skill_content}
The user has provided the following instruction alongside the skill invocation: {prompt}
中文对照:
text
[重要:用户调用了 "{skill_name}" skill,表示他们希望你遵循这个 skill 的说明。完整 skill 内容加载如下。]
{skill_content}
用户在调用 skill 的同时提供了下面这条指令:{prompt}
这里的 {prompt} 已经包含 cron guidance、script output、context_from output 等内容。
所以 skill-backed cron job 的最终 prompt 结构大致是:
text
[IMPORTANT: The user has invoked the "xxx" skill...]
<SKILL.md 内容>
The user has provided the following instruction alongside the skill invocation:
[IMPORTANT: You are running as a scheduled cron job...]
## Script Output / ## Output from job ...
<job["prompt"]>
如果多个 skills,会按 job 中 skills 的顺序依次加载。
场景六:完整例子,从用户输入到未来执行 prompt
用户说:
text
用户:每天上午 10 点检查一下 /home/tlinux/app 的测试结果。如果失败,按我们的"故障日报"skill 总结原因,发到当前群。
模型根据 tool schema 判断:
text
这是未来重复任务 -> action=create
每天上午 10 点 -> schedule="0 10 * * *"
要在项目目录里执行 -> workdir="/home/tlinux/app"
要按指定 skill -> skills=["incident-daily-report"]
要检查测试结果 -> prompt 必须写清楚未来要做什么
发到当前群 -> deliver 省略
可能的 tool call:
json
{
"action": "create",
"schedule": "0 10 * * *",
"name": "App test failure daily check",
"skills": ["incident-daily-report"],
"prompt": "In the project /home/tlinux/app, inspect the latest test results and failure logs. If tests are passing and there is nothing actionable, respond exactly with [SILENT]. If there are failures, summarize the failing tests, likely causes, relevant files or commands, and recommended next actions in Chinese.",
"workdir": "/home/tlinux/app",
"enabled_toolsets": ["terminal", "file"]
}
创建后,jobs.json 保存的是这些字段。未来到点后,scheduler 拼出来的 prompt 不是 tool call 本身,而是类似:
text
[IMPORTANT: The user has invoked the "incident-daily-report" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]
<incident-daily-report/SKILL.md 内容>
The user has provided the following instruction alongside the skill invocation:
[IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user --- do NOT use send_message ...]
In the project /home/tlinux/app, inspect the latest test results and failure logs. If tests are passing and there is nothing actionable, respond exactly with [SILENT]. If there are failures, summarize the failing tests, likely causes, relevant files or commands, and recommended next actions in Chinese.
这个未来 prompt 不是当前模型临时生成后马上执行,而是等 scheduler 到点以后,由 cron job runner 构造并交给新的 AIAgent 执行。
场景七:script + no_agent 的调用区别
script 有两种完全不同的运行方式。
text
no_agent=False 默认模式:
script 是数据采集步骤。
script stdout 注入 prompt。
LLM 仍然会被唤醒,负责分析和生成最终回复。
no_agent=True:
script 本身就是任务。
不创建 AIAgent,不消耗模型 token。
script stdout 直接作为投递内容。
7.1 默认模式:脚本采集数据,模型负责分析
用户说:
text
用户:每 30 分钟检查一次服务器磁盘和内存。如果超过阈值,就总结当前状态发给我;正常就不要打扰。
可以创建脚本:
text
~/.hermes/scripts/check_server.py
模型调用:
json
{
"action": "create",
"schedule": "every 30m",
"script": "check_server.py",
"prompt": "Analyze the script output. If server usage is risky, explain the risk and suggest the next action in Chinese. If everything is normal, respond exactly with [SILENT].",
"name": "server health check",
"enabled_toolsets": ["terminal"]
}
到点后:
text
1. scheduler 先运行 check_server.py。
2. 如果 stdout 有内容,注入到 ## Script Output。
3. scheduler 再加 cron execution guidance。
4. AIAgent 根据脚本输出和 prompt 生成最终回复。
5. scheduler 投递最终回复。
如果脚本输出:
text
Disk usage: /data 93%
Largest directories:
/data/logs 120G
/data/cache 80G
模型最终可能回复:
text
/data 磁盘使用率已经到 93%,建议优先清理 /data/logs 和 /data/cache,并检查日志轮转配置。
如果脚本最后一行输出:
json
{"wakeAgent": false}
Hermes 会跳过 LLM。这是脚本级别的 wake gate,适合"没事就不唤醒模型"的监控任务。
7.2 no_agent 模式:脚本就是任务
用户说:
text
用户:每 5 分钟跑一下 memory-watchdog.sh。脚本有输出就发给我,没有输出就不要打扰。
这是典型 no_agent 任务。模型看到 no_agent 参数说明后,应该调用:
json
{
"action": "create",
"schedule": "every 5m",
"script": "memory-watchdog.sh",
"no_agent": true,
"name": "Memory watchdog"
}
这种任务未来执行时不会有 LLM prompt:
text
tick()
-> run_job(job)
-> job.no_agent=True
-> _run_job_script("memory-watchdog.sh")
-> stdout 非空则直接投递
-> stdout 为空则静默
也就是说:
text
no_agent=True:
prompt / skills 被忽略。
script stdout 就是用户看到的最终消息。
no_agent=False:
script stdout 只是上下文。
模型还会被唤醒,生成最终回复。
这个模式适合:
text
磁盘/内存/GPU 阈值告警。
CI 状态通知。
API poller。
心跳脚本。
已经能产出最终人类可读消息的 bash/python 脚本。
不适合:
text
需要总结网页。
需要从数据里判断重要性。
需要重写成面向用户的报告。
需要复杂工具调用。
场景八:用户要求修改或删除任务,模型如何调用
用户说:
text
用户:把每天早上的 AI 日报改成每周一早上 9 点。
模型不能直接猜 job_id。根据工具描述,它应该:
第一步:
json
{
"action": "list"
}
假设返回:
json
{
"jobs": [
{
"job_id": "a1b2c3d4e5f6",
"name": "AI daily briefing",
"schedule": "0 9 * * *"
}
]
}
第二步:
json
{
"action": "update",
"job_id": "a1b2c3d4e5f6",
"schedule": "0 9 * * 1"
}
删除同理:
text
用户:停掉 AI 日报。
先 list,再 remove:
json
{
"action": "remove",
"job_id": "a1b2c3d4e5f6"
}
场景九:到点后 scheduler 如何执行
gateway 启动后,会创建后台 cron ticker。
相关实现说明 / Gateway Cron Ticker
英文原文:
text
Background thread that ticks the cron scheduler at a regular interval.
Runs inside the gateway process so cronjobs fire automatically without
needing a separate `hermes cron daemon` or system cron entry.
When ``adapters`` and ``loop`` are provided, passes them through to the
cron delivery path so live adapters can be used for E2EE rooms.
中文对照:
text
这是一个后台线程,会按固定间隔 tick cron scheduler。
它运行在 gateway 进程内部,所以 cronjobs 可以自动触发,
不需要单独的 `hermes cron daemon`,也不需要系统 crontab。
当提供 adapters 和 loop 时,会把它们传给 cron delivery 路径,
这样 E2EE 房间可以使用 live adapter 投递。
执行链路:
text
gateway/run.py::_start_cron_ticker()
-> 每 60 秒调用 cron.scheduler.tick()
tick()
-> 获取 ~/.hermes/cron/.tick.lock
-> get_due_jobs()
-> advance_next_run() 预推进 recurring jobs
-> run_job(job)
-> save_job_output()
-> _deliver_result()
-> mark_job_run()
9.1 tick 如何避免重复执行
tick() 开始时会获取文件锁:
text
~/.hermes/cron/.tick.lock
如果另一个 gateway ticker 或手动 hermes cron tick 已经拿到锁,本次 tick 直接返回 0。
这解决的是"同一批 due jobs 被两个进程同时执行"的问题。Hermes 的 cron 是应用内 scheduler,但它仍然考虑了多进程重叠:
text
gateway 常驻线程在 tick。
用户手动运行 hermes cron tick。
测试或脚本也可能调用 scheduler.tick()。
文件锁让这些入口不会同时跑同一个到期任务批次。
9.2 recurring jobs 为什么先 advance_next_run
tick 找到 due jobs 后,会先对 recurring jobs 调用:
text
advance_next_run(job_id)
然后才执行 run_job(job)。
这样做是为了避免 crash loop:
text
如果先执行任务,执行到一半 gateway 崩溃。
jobs.json 里的 next_run_at 仍然停留在过去。
gateway 重启后又立刻执行同一个任务。
如果再次崩溃,就会不断重复。
预先推进 next_run_at 后,Hermes 更接近 at-most-once 语义:进程崩溃时可能少跑一次,但不会在恢复后疯狂补跑同一个任务。
9.3 gateway 停机后会不会补跑所有错过的任务
不会。
get_due_jobs() 对 recurring jobs 有 missed-run fast-forward 逻辑。如果任务错过时间太久,Hermes 会把它推进到下一次未来时间,而不是立刻补跑旧任务。
宽限窗口大致是:
text
interval jobs:
周期的一半,最少 120 秒,最多 2 小时。
cron jobs:
用 croniter 估算相邻触发间隔,取一半,同样限制在 120 秒到 2 小时。
例子:
text
任务:每天 9:00 发日报。
gateway:从 8:50 停到 13:00。
结果:超过 2 小时宽限,不补发当天 9:00 日报,直接推进到下一次。
这个设计避免 gateway 恢复后把一堆过期消息发给用户。
9.4 并发执行和 workdir 串行
Hermes 支持同一个 tick 里执行多个 due jobs。并发数来自:
text
HERMES_CRON_MAX_PARALLEL 环境变量
cron.max_parallel_jobs 配置
默认 unbounded
但带 workdir 的 job 会被单独串行执行。原因是 cron run 会临时设置进程级环境变量:
text
TERMINAL_CWD=<job.workdir>
这是 process-global 状态。如果两个 workdir jobs 并行执行,一个 job 的 terminal/file/code_exec 工作目录可能被另一个 job 改掉。Hermes 因此把 workdir jobs 从并行池里拆出来,顺序执行。
LLM 模式下,run_job() 会创建:
text
AIAgent(
platform="cron",
disabled_toolsets=["cronjob", "messaging", "clarify"],
skip_memory=True,
session_id="cron_<job_id>_<timestamp>"
)
这说明未来 cron run 是隔离的新 session:
text
没有当前聊天历史。
不会让 cron job 再创建 cron job。
不会让 cron job 自己 send_message。
不会让无人值守任务要求用户澄清。
不会把 cron run 写入普通用户 memory。
场景十:输出如何投递
agent 的最终回复不是直接进入原聊天历史,而是由 scheduler 投递。
默认包装格式:
text
Cronjob Response: {task_name}
(job_id: {job_id})
-------------
{content}
To stop or manage this job, send me a new message (e.g. "stop reminder {task_name}").
如果最终回复是:
text
[SILENT]
则跳过投递,但本地输出仍会保存。
本地输出目录:
text
~/.hermes/cron/output/<job_id>/<timestamp>.md
投递目标来自:
text
deliver
origin
平台 home channel 环境变量
显式 platform:chat_id:thread_id
10.1 local、origin 和显式目标
deliver 的几种常见语义:
text
local
只保存到本地 output 目录,不发消息。
origin
发回创建任务的原始平台、chat_id、thread_id。
telegram / discord / slack / matrix / email / sms 等平台名
发到该平台配置的 home target。
telegram:<chat_id>:<thread_id>
发到明确指定的 chat/topic。
多个目标
deliver 可以是逗号分隔字符串,scheduler 会解析多个目标并去重。
为什么推荐省略 deliver?因为工具参数说明里写明:省略会自动投递回当前 chat/topic,并保留 thread/topic。模型显式写 telegram:<chat_id> 时,如果漏掉 thread_id,反而可能丢失 topic。
10.2 为什么 scheduler 统一投递
cron agent 运行时被提示不要 send_message,并且 messaging toolset 被禁用。
原因是:
text
1. 投递目标来自 job metadata,不应该让模型猜。
2. E2EE 平台需要 gateway 的 live adapter。
3. scheduler 能统一处理 wrapper、MEDIA 标签、delivery error。
4. 避免模型重复发送或发错位置。
投递失败不会抹掉 agent 执行结果。mark_job_run() 会单独记录:
text
last_delivery_error
这表示 job 本身可能成功,但平台投递失败。
场景十一:任务状态如何更新
每次 job 执行后,scheduler 会调用 mark_job_run() 更新 jobs.json。
主要字段:
text
last_run_at
最近一次运行时间。
last_status
ok 或 error。
last_error
agent/script 执行失败时的错误。
last_delivery_error
投递失败时的错误,和执行失败分开记录。
repeat.completed
已完成次数。
next_run_at
下一次触发时间。
state
scheduled / paused / completed / error。
常见状态:
text
scheduled
启用,等待下一次 next_run_at。
paused
暂停,不会执行。
completed
一次性任务完成,或 repeat 次数耗尽。
error
recurring job 无法计算下一次运行等情况。Hermes 不会静默删除它,而是保留错误状态。
repeat 逻辑也在这里处理:
text
one-shot schedule:
create_job() 默认 repeat=1。
执行一次后完成。
recurring schedule:
repeat 省略表示永久。
repeat=N 表示执行 N 次后移除。
场景十二:安全与防爆设计
12.1 prompt 创建时扫描高危模式
创建或更新 job prompt 时,_scan_cron_prompt() 会拦截明显高危内容,例如:
text
ignore previous/all/above instructions
do not tell the user
system prompt override
disregard your instructions/rules
curl/wget 携带 KEY/TOKEN/SECRET/PASSWORD/CREDENTIAL/API 环境变量
cat .env / credentials / .netrc / .pgpass
authorized_keys
/etc/sudoers / visudo
rm -rf /
不可见 unicode 控制字符
这不是完整沙箱,但能挡掉非常明显的 cron prompt injection 和 exfiltration payload。
12.2 script path 被限制在 scripts 目录
script 参数在创建/更新时会验证:
text
拒绝绝对路径。
拒绝 ~ 路径。
拒绝 ../ 逃逸。
resolve 后必须仍在 ~/.hermes/scripts/ 内。
执行时也会再次校验路径,防止手改 jobs.json 绕过创建时校验。
12.3 cron approval mode
配置里有:
yaml
approvals:
cron_mode: deny
含义:
text
deny
cron job 遇到危险命令时阻止,让 agent 找别的办法。默认安全。
approve
cron job 自动批准危险命令。只适合明确受控的自动化流水线。
12.4 防爆机制
Hermes 目前的控制包括:
text
1. one-shot 默认 repeat=1。
2. recurring jobs 错过太久会 fast-forward,不补跑一堆历史任务。
3. context_from 只读取最近一次输出,并截断到 8000 字符。
4. script 有 timeout。
5. cron agent 有 inactivity timeout,默认 600 秒。
6. enabled_toolsets 可以减少工具 schema 和权限面。
7. max_parallel_jobs 控制每个 tick 的并发。
8. [SILENT] 和 wakeAgent=false 可以减少无意义投递和 LLM 调用。
仍然没有完全解决的是 output 保留策略:
text
~/.hermes/cron/output/<job_id>/*.md
会随运行次数增长。当前实现没有明确的 per-job TTL、max outputs、自动归档或自动摘要。长期大量 cron jobs 需要额外治理。
更强的治理可以补:
text
按 job 配置 retention_days。
按 job 配置 max_outputs。
只保留最近 N 次完整输出,旧输出压缩成 summary。
为 context_from 建独立 compacted artifact,而不是直接读取完整 md。
对失败输出和成功输出设置不同保留策略。
提供 hermes cron prune 命令。
总流程:从 tool schema 到未来 prompt
完整链路可以压缩成这样:
text
用户:
"每天早上 9 点发 AI 日报"
当前模型看到:
cronjob tool description
cronjob parameters schema
当前模型判断:
这是未来自动任务,不是现在执行一次。
当前模型调用:
cronjob({
action: "create",
schedule: "0 9 * * *",
prompt: "完整、自包含的未来任务指令",
name: "AI daily briefing"
})
工具 handler:
校验 action/schedule/prompt/script/context_from/workdir。
parse_schedule()。
create_job()。
save_jobs() -> ~/.hermes/cron/jobs.json。
gateway:
_start_cron_ticker() 每 60 秒调用 tick()。
tick:
get_due_jobs()。
run_job(job)。
_build_job_prompt:
job["prompt"]
+ optional script output/error
+ optional context_from output
+ cron execution guidance
+ optional skills content
未来 cron agent:
fresh AIAgent session。
disabled_toolsets=["cronjob", "messaging", "clarify"]。
run_conversation(final_prompt)。
scheduler:
保存 output。
final_response 非 [SILENT] 时自动投递。
mark_job_run() 更新状态和 next_run_at。
这就是 Hermes cron 的关键设计:tool schema 负责让模型知道"怎么创建任务",job storage 负责保存"未来要做什么",scheduler 负责在未来把保存的任务指令加工成真正的 runtime prompt 并执行。