Repository navigation
[RFC] Conversation-created schedules and goal-backed scheduled runs #6131
Description
Summary and status
This continues two items #3892 left for later: Phase 4, a schedule_task tool so a task can be created from a conversation, and the Phase 5 item "Optional goal-backed long-running scheduled work". #3834 also put scheduled goals out of scope.
Nothing is built. Before writing code I would like a short answer to eight questions (details under Questions):
- Agent-created tasks: active at once (my lean), or created paused?
- Inside a scheduled run: no schedule tool at all, or only "stop my own schedule" (my lean)?
- A run that finished without meeting its goal: a new occurrence status
unmet(my lean),failedwith the reason, orsuccessplus an outcome field? - In runs that cannot ask the user, may the goal evaluator accept a low-risk assumption that the final answer states (my lean: yes)? For all such runs, or scheduled runs only?
- Limits for tool-created tasks: at most 20 live (enabled, running or paused) per user, checked at creation, and an end condition for anything that fires more than hourly. Acceptable?
- A recurring goal task is paused after 3 consecutive
unmetoccurrences. Acceptable? - In this PR, results of a tool-created task that runs on a fresh thread stay in that thread and on the tasks page; they are not posted back into the DeerFlow chat it was created from. IM delivery is unchanged: feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135's notification reaches the owner's connected channels as for any scheduled task (proactive push is implemented for WeCom only). Acceptable?
- Three small additions (the previous occurrence as a reference, standing notes, a trial-run offer): keep all of them in the first PR (my lean), or leave some for later?
Cost: I would send one PR, with one migration that adds nullable columns only. The two halves do not depend on each other, so if you prefer two PRs (the tool first, goal-backed runs second) I will split it. The notification outbox from #4843 has since landed as #6135. I would start after #5645 is settled, since it touches the run worker and the scheduled-run launch path and adds migrations.
Running cost: a goal occurrence can take up to 9 agent turns plus an evaluator call after each, so the continuation cap bounds turns, not spend. By code, the per-run token budget (token_budget, off by default, 200k tokens when on) also ends a goal occurrence, because continuations run inside the same run (#5424); I have not exercised that in a scheduled run yet. The PR will include both: a scheduled goal run under a small budget, and a matched comparison on the same set of scheduled tasks, plain runs against goal runs, reporting completion rate, turns, tokens and cost from the run rows, measured in a real Gateway. If goal runs do not complete more tasks for the extra spend, I will say so and the goal half can be dropped or narrowed.
cc @fancyboi999 (author of #3892), @hetaoBackend (author of #3834), @WillemJiang
What main does today
Read on 4c05e88c. An occurrence is one row per firing of a task; a goal stands down when the evaluator stops continuing without the goal being met.
- No way to give a scheduled run a goal. The scheduler has no goal field.
PUT /api/threads/{id}/goalreturns 409 while a run is in flight, and afresh_thread_per_runtask picks its thread id only at dispatch. Areuse_threadtask on a thread that already holds a goal is evaluated against that goal, because the goal is thread-scoped: nothing tells the evaluator the run was scheduled, and the continuation count carries over between occurrences (only a new/goalresets it). - The goal has one status.
GoalState.statusisLiteral["active"]. A satisfied goal is removed; a stood-down goal staysactivewithlast_evaluation.stand_down_reason. - The completion hook cannot see the evaluator's result.
handle_run_completionmaps run status to occurrence status. A run whose goal stood down still endssuccess, so the occurrence sayssuccess. Three recovery functions (recover_expired_launch_claims,mark_stale_active_runs,reconcile_active_runs) derive the occurrence status from the run row (therunstable) in the same way. - The non-interactive run prompt and the evaluator disagree. The prompt, shared by scheduled, webhook and autonomous runs, says for low-risk and reversible work to "make the smallest reasonable assumption and continue" and to "State every material assumption in the final result"; for high-risk or irreversible work without sufficient authorization, to "stop with a concise structured
BLOCKEDresult". The evaluator prompt has no run-mode branch: "If the assistant assumed, guessed or substituted for missing or ambiguous information, the goal is not met: use blocker needs_user_input." - Limits.
max_concurrent_runsdefaults to 3 and is global, not per user.intervalhas a configurable floor (min_once_delay_seconds, default 60 s);cronis not checked against it, and being five-field its floor is one minute. There is no cap on tasks per user and no end condition:run_countis only incremented, and themax_runsfield proposed in [RFC] First-class scheduled runs and cron automation #3892 is not implemented. - Switching a task off. Of the tasks page's actions, pause, resume and delete switch a task off. A task definition is immutable while an occurrence is
queued,launchingorrunning(backend/docs/CONFIGURATION.md), so pause and delete return 409 while one is launching or running. The chat page has a "Scheduled tasks" button that opens the tasks page filtered to that thread.
Two examples
- Ongoing: "Every Friday, write the weekly report from these project documents." Each Friday's report is one occurrence; when it meets its goal, that occurrence is done and the schedule carries on.
- Finite: "Every 30 minutes, check whether these materials are all in; once they are, write the meeting prep; stop by the time the meeting starts." Once the prep is written, the agent stops its own schedule; the meeting start is the end time that bounds it if it never does.
A goal met on one occurrence never ends a recurring task by itself. The agent stops the schedule only according to the end condition the user stated.
Proposal
1. schedule_task tool
Behind config as #3892 requires, off by default, and not offered when scheduler.enabled is false.
- Placement. The tool lives in the harness and imports nothing from
app. It calls a small scheduler capability that the Gateway installs at startup, asset_mcp_task_submitterdoes today. The create-time checks now in the REST route move into that capability, so the route and the tool share them. - In a chat: create, list, pause, delete, with the same schedule types and checks as the REST API (
once,interval,cron); [RFC] First-class scheduled runs and cron automation #3892 suggested one-time and simple presets first, so please say if you want it narrower. Pause and delete keep the existing 409 rule. Create is offered only in runs that can ask the user, resolved throughRunInteractionPolicylikeask_clarification, so webhook, autonomous and scheduled runs never get it. When the request is ambiguous the agent asks; this covers [RFC] First-class scheduled runs and cron automation #3892's "The tool should not silently create indefinite cron jobs from ambiguous language" by its first option, without a draft state. - Context mode. A tool-created task runs
fresh_thread_per_rununless the user asks for the results in this chat; then it isreuse_threadon the current thread, and it cannot carry a goal (see 2). - Scope. The tool manages only tasks it created from the current thread. The task records that thread in a new nullable column, and
GET /api/threads/{id}/scheduled-tasksreturns it too, so the chat's Scheduled tasks button finds it. - Tool result. It returns the task id, schedule, next run time, end condition and how to stop it, as [RFC] First-class scheduled runs and cron automation #3892 asks, and the tool description tells the agent to repeat them. "How to stop it" is the tasks page or asking in the chat; in an IM channel, asking in the chat.
- Inside a scheduled run there is no create, list, pause or delete. One action is left for tool-created tasks: stop my own schedule. [RFC] First-class scheduled runs and cron automation #3892's rule that scheduled agents "cannot recursively create schedules unless explicitly allowed" is kept. Because a task definition is immutable while an occurrence is active, the action does not change the task mid-run: it records the request on the run, and the code that finalizes the occurrence (the completion hook, or a recovery function after a restart) pauses the task in the same transaction, with
last_errorset to "stopped by the agent in run ". Nothing is deleted; the user can resume. - End conditions. Optional
max_runs(counting scheduled occurrences only; manual triggers, including a trial run, do not count) and an optional end time; when either is reached the task becomescompleted. A tool-created schedule that fires more than hourly (anintervalunder 3600 s, or acronwhose minute field is not a single number) must carry one of them. The agent takes it from the conversation, or asks when the user gave none. End conditions are checked where the occurrence is finalized and again at dispatch, so a task resumed after its end time is completed without running; when an end condition and the auto-pause below fire on the same occurrence, the end condition wins.
2. Run as goal
- Where. The tool takes an optional goal objective, separate from the prompt: a checkable outcome the agent writes from the user's words. Without one the task has no goal. In this PR only
fresh_thread_per_runtasks may carry one: on a reused thread the scheduled goal would replace a goal the user set there, and the evaluator's evidence window would span earlier occurrences. - Lifetime. The worker sets the goal before the first turn, from the objective in the run's metadata, and removes it when the run ends, whatever the terminal status. Each occurrence starts with fresh counters and uses the existing caps (8 continuations, 2 without progress).
- Evaluator. For runs that cannot ask the user (selected through
RunInteractionPolicy, so webhook and autonomous runs are included unless you prefer scheduled runs only), the evaluator's rule on assumptions quoted above is replaced by: "This run had no user to ask. A low-risk, reversible assumption about a detail the request left open, stated in the final answer, is not by itself a reason to fail the goal. It does not replace evidence that the objective was achieved, and an assumption that is not stated still fails it." The evaluator prompt also says that a final answer which is aBLOCKEDresult means blockerneeds_user_input; there is no parser for it. The run prompt asks for one line per assumption: what was unclear, what was chosen, why. - Outcome.
GoalStatein thread state is unchanged. The final verdict is written to the run row in the same write as the terminal status, in a new nullable column (notstop_reason, which already holds cap and recovery reasons that can occur together with a verdict). The completion hook and the recovery functions read it. - New occurrence status
unmet: the run finished, the goal was not met. A goal task's run that endssuccesswithout a satisfied verdict isunmet. The reason (the existingstand_down_reason, orno_verdictwhen the run ended without an evaluation) goes into the occurrence's existingerrorcolumn, so run history shows it. Aoncetask whose occurrence isunmetbecomesfailed.unmetjoins the status enum and its terminal set, the history filter, and the frontend type and labels. Through feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135's outbox, anunmetoccurrence gets its own notification text naming the reason code: therun_failedtext deliberately carries no error detail to IM, while the reason codes are DeerFlow's own vocabulary, not raw errors. The migration's downgrade rewritesunmettofailed, as 0015's downgrade rewriteslaunchingtoqueued. - Auto-pause. When a scheduled occurrence of a recurring goal task ends
unmetand the two scheduled occurrences before it also endedunmet, the task is paused with the reason inlast_error. Asuccessresets the streak, including one that relied on a stated assumption (see Amendments).failed,interruptedandskippedoccurrences, manual triggers, andunmetwith reasonexternal_waitare skipped over. The check runs where the occurrence is finalized, so a resumed task always runs again, and one moreunmetpauses it again. All parent-task changes in this proposal (the stop request, auto-pause, end conditions) are applied insidecomplete_run, and only on the first terminal write to an occurrence.complete_rundoes not enforce that today: a second call for the same occurrence and run overwrites its status and returnsTrue; only the launch accounting is guarded. This PR adds the check, so a late duplicate completion callback cannot apply a stop twice or re-pause a task the user has resumed. An auto-pause also gets its own notification text; the current renderer would show any event other thanrun_failedas "Scheduled task completed".
3. Three additions
Each is small, needs no frontend change, and can be dropped on its own (question 8).
- The previous occurrence as a reference. For a recurring task on fresh threads, every occurrence after the first gets the previous occurrence's thread as a read-only conversation reference, the mechanism a user gets by @-referencing a conversation ([RFC] Cross-session conversation references and retrieval #5398). The run carries one hidden line naming that thread, and
read_conversationreads it on demand, with the same strict ownership check and visible messages only. The agent decides whether it helps: a report can say what changed since last time, a status check can compare against the last state, a reminder ignores it. Nothing is configured per task. Today only user-submitted runs can carry references and the reader is bound to an HTTP request's authority, so the scheduled launch prepares it with the task owner's authority instead; it applies only whenread_conversationis enabled. A run with nothing new to report still notifies; letting it stay quiet is a possible follow-up. - Standing notes. In a chat, the user can tell the agent to remember something for a task it created ("use the develop branch for the weekly report"). The tool's new note action stores it on the task, and every later occurrence receives the notes appended to its scheduled prompt, which is already a user message, so a note carries no more authority than the prompt itself. Notes are added only on the user's explicit words, never inferred by the agent; the list action shows them; at most 10 notes of 500 characters each. This turns a stated assumption into a one-time correction.
- A trial-run offer. After creating a recurring task whose runs produce something the user would want to look at first (a report, a digest, a file), the agent offers to run it once now. Tasks that watch for a condition and one-time tasks get no offer. The trial uses the existing manual trigger, the same one behind the tasks page's run-now button, so it neither counts toward the auto-pause streak nor uses up
max_runs.
Not in this PR
More GoalState statuses (#3834 sketched four; main has one and this keeps it); completing a task on the evaluator's verdict (the stop action in 1 is the agent's own call, and the only automatic reaction is the auto-pause above); posting a fresh-thread result back into the DeerFlow chat it was created from (#3892 Phase 4 lists "SSE/thread refresh for scheduled results"; IM delivery through #6135 is unaffected); goal tasks on reused threads; a confirmation card in the chat; showing or editing the goal and end condition on the tasks page; goal evaluation in the TUI or embedded client.
Frontend changes are limited to the label and type entry for unmet.
Acceptance
- End to end on a real Gateway: a task created through the tool from a real conversation fires on schedule, runs on a fresh thread, reads the sources it was given and produces a file that opens and says the right thing; the agent then stops its own schedule, and the next due time dispatches nothing.
- The stop survives a Gateway restart between the request and the end of the run, and a duplicate completion callback does not apply it twice.
- Notifications are reported only for channels checked end to end. A delivery row written to the outbox is not counted as received; I have no WeCom account, so the PR will say how far that check went.
- A second occurrence reads the first through the reference, and a note added in chat is followed by the next occurrence.
- The running-cost comparison described under Cost.
Questions
- Active at once, or created paused? [RFC] First-class scheduled runs and cron automation #3892 open decision 5: "Should conversation-created recurring schedules require explicit UI confirmation before activation?" I lean to active: the user asked for the task directly, and the tool result says how to stop it. The other option needs no new UI: the task is created
pausedand the user presses Resume. - Inside a scheduled run: no schedule tool at all, or only "stop my own schedule"? Creation is unavailable either way. I lean to the second, so a run that finds the job finished can stop its own schedule.
- A run that finished without meeting its goal:
unmet,failedwith the reason, orsuccessplus an outcome field? [RFC] First-class scheduled runs and cron automation #3892's checklist says "UI never hides a failing scheduler state behind a successful-looking task row", which rules out the third for me.failedis the cheapest (no new status, no downgrade rewrite, no frontend change). I lean tounmetbecause the next step differs: afailedrun means something broke, anunmetrun finished and its result needs reading or the goal adjusting, and the auto-pause has to tell them apart. Underunmet, feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135's outbox sends a distinct notification naming the reason; underfailed, it sends the generic failure notice, which by design carries no error text. - In runs that cannot ask the user, may the goal evaluator accept a low-risk assumption that the final answer states? I lean to yes, with the wording in section 2. For all such runs, or scheduled runs only? Applying it to all of them changes how existing webhook and autonomous goal runs are judged, so it would ship with an upgrade note and its own tests.
- Limits for tool-created tasks (the tasks page and REST API stay as they are): at most 20 live tool-created tasks per user, where live means enabled, running or paused, so pausing does not free a slot and resuming on the tasks page needs no check; the count and the insert happen in one transaction. And an end condition for anything that fires more than hourly. I would start with constants rather than
scheduler.*config. Acceptable, or other numbers? - Auto-pause after 3 consecutive
unmetoccurrences of a recurring goal task, as described in section 2. Onmainnothing pauses a recurring task automatically, so this would be the first such rule. Acceptable? - Results of a fresh-thread task stay in its own thread and on the tasks page in this PR, with no post-back into the DeerFlow chat it was created from. IM delivery is separate and unchanged: feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135 notifies the owner's connected channels for every scheduled occurrence, whichever thread it ran on, so a task created from an IM chat still reports back there (WeCom only today; other bound channels record a failed delivery). Acceptable, or should post-back into the web chat be part of it?
- The three additions in section 3 are separable. I lean to keeping all of them in the first PR, since none needs a frontend change and together they are what makes a recurring task worth keeping. Would you rather leave any for later?
Materials
- Earlier RFCs: [RFC] First-class scheduled runs and cron automation #3892 (scheduled runs), [RFC] Claude Code-style /goal with evaluator-driven continuation #3834 (
/goal).
Amendments
- Accepted in the reply to the first comment: the tool result quotes the goal objective verbatim; the scheduled-run verdict records whether a satisfied verdict relied on a stated assumption, which is shown to the user but does not change the auto-pause rule; an auto-pause is delivered through feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135's outbox with its own text and the reason.
- Corrected:
complete_rundoes not yet reject a second terminal write to an occurrence (see Run as goal);unmetneeds its own notification text because therun_failedtext omits error detail; the notification dependency is feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135, which carried feat(scheduler): push scheduled task results to IM channels via a notification outbox #4843.
[RFC] 在对话中创建定时任务,以及以 goal 运行的定时任务
概要与状态
本提案接续 #3892 留待后续的两项:第 4 阶段的 schedule_task 工具(在对话中创建任务),以及第 5 阶段中的 "Optional goal-backed long-running scheduled work"。#3834 也把定时 goal 列为范围之外。
目前没有写任何代码。动手之前,我想请大家简短回答八个问题(细节见"问题"一节):
- 智能体创建的任务:立即生效(我的倾向),还是创建为暂停?
- 在定时运行内部:完全不提供定时工具,还是只允许"停止我自己的调度"(我的倾向)?
- 运行结束但 goal 未达成:新增执行记录状态
unmet(我的倾向)、记为failed并附原因,还是保留success并另加一个结果字段? - 在无法询问用户的运行中,评估器可否接受最终回答里已说明的低风险假设(我的倾向:可以)?适用于所有这类运行,还是只限定时运行?
- 工具创建的任务的上限:每个用户最多 20 个存续中(启用、运行中或暂停)的任务,在创建时检查;触发频率高于每小时一次的必须带结束条件。可以接受吗?
- 周期性的 goal 任务连续 3 次
unmet后暂停。可以接受吗? - 本 PR 中,在新对话里运行的工具创建任务,其结果留在该对话和任务页,不回帖到创建它的 DeerFlow 对话。IM 投递不变:feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135 的通知会像其他定时任务一样送到任务所有者已连接的渠道(目前只有企业微信实现了主动推送)。可以接受吗?
- 三项小的附加功能(引用上一次执行、长期备注、主动提出试跑):都放进第一个 PR(我的倾向),还是留一部分以后做?
代价:我打算提交一个 PR,其中一个迁移,只新增可空字段。两部分互不依赖,如果你们更希望拆成两个 PR(先工具,后 goal 运行),我就拆。#4843 的通知 outbox 已作为 #6135 合入。我会等 #5645 有结论后再开始,因为它涉及运行 worker 和定时运行的启动路径,并新增了迁移。
运行成本:一次 goal 执行最多 9 轮智能体回合,每轮之后还有一次评估器调用,因此续跑上限约束的是轮数,不是花费。从代码看,每 run 的 token 预算(token_budget,默认关闭,开启时默认 20 万 token)同样会终止 goal 执行,因为续跑在同一个 run 内进行(#5424);这一点我还没有在定时运行中实际验证。PR 会附两项:一次小预算下的 goal 定时执行,以及同一组定时任务上普通运行与 goal 运行的对照,从 run 记录统计完成率、轮数、token 和费用,在真实 Gateway 中测得。如果 goal 运行多花的钱没有换来更多完成的任务,我会如实说明,goal 这一半可以砍掉或收窄。
main 目前的行为
基于 4c05e88c 阅读。"执行记录"(occurrence)指任务每触发一次产生的一行;goal "中止续跑"(stand down)指评估器在 goal 未达成的情况下停止继续。
- 没有给定时运行设 goal 的途径。 调度器没有 goal 字段。
PUT /api/threads/{id}/goal在运行进行中返回 409,而fresh_thread_per_run任务的对话 ID 在派发时才确定。reuse_thread任务如果所在对话已有 goal,会按这个 goal 被评估,因为 goal 是对话级的:评估器并不知道这次运行来自调度器,续跑计数也会跨执行累计(只有重新/goal才归零)。 - goal 只有一个状态。
GoalState.status是Literal["active"]。达成的 goal 被移除;中止续跑的 goal 仍为active,原因记在last_evaluation.stand_down_reason。 - 完成回调看不到评估器的结果。
handle_run_completion把运行状态映射为执行记录状态。goal 中止续跑的运行仍以success结束,执行记录也就是success。三个恢复函数(recover_expired_launch_claims、mark_stale_active_runs、reconcile_active_runs)同样根据 run 行(runs表)推导执行记录状态。 - 非交互式运行的提示词与评估器不一致。 该提示词由 scheduled、webhook、autonomous 三种运行共用:对低风险且可逆的工作 "make the smallest reasonable assumption and continue",并且 "State every material assumption in the final result";对未获充分授权的高风险或不可逆工作则 "stop with a concise structured
BLOCKEDresult"。评估器提示词不区分运行模式:"If the assistant assumed, guessed or substituted for missing or ambiguous information, the goal is not met: use blocker needs_user_input." - 上限。
max_concurrent_runs默认为 3,是全局的而非每用户的。interval有可配置的下限(min_once_delay_seconds,默认 60 秒);cron不受该配置约束,因为是五段式,下限就是一分钟。没有每用户任务数上限,也没有结束条件:run_count只增不比较,[RFC] First-class scheduled runs and cron automation #3892 提出的max_runs字段未实现。 - 关掉任务。 任务页的操作里,暂停、恢复和删除用于关掉任务。执行记录处于
queued、launching或running时任务定义不可修改(backend/docs/CONFIGURATION.md),因此执行正在启动或运行时,暂停和删除返回 409。聊天页有"定时任务"按钮,打开按该对话筛选的任务页。
两个例子
- 持续任务:「每周五根据这些项目资料写周报。」每周五的周报是一次执行;它达成 goal,这次执行就结束,调度继续。
- 有限任务:「每半小时检查这些材料是否齐全;齐全后写会议准备稿;最晚到会议开始前结束。」准备稿写完后,智能体停止自己的调度;会议开始时间是截止时间,万一它没有停,也会到此为止。
单次执行达成 goal,本身从不结束周期任务。智能体只按用户说明的结束条件停止调度。
提案
1. schedule_task 工具
按 #3892 的要求由配置控制,默认关闭;scheduler.enabled 为 false 时不提供。
- 位置。 工具放在 harness 中,不引用
app。它调用 Gateway 启动时安装的一个小型调度能力,做法与现有的set_mcp_task_submitter相同。目前写在 REST 路由里的创建校验移入该能力,路由和工具共用。 - 在对话中: 创建、列出、暂停、删除,调度类型和校验与 REST API 相同(
once、interval、cron);[RFC] First-class scheduled runs and cron automation #3892 建议先支持一次性和简单预设,如果希望收窄请告诉我。暂停和删除沿用现有的 409 规则。创建只在能询问用户的运行中提供,与ask_clarification一样通过RunInteractionPolicy决定,因此 webhook、autonomous 和定时运行都拿不到。请求含糊时智能体提问;这样按 [RFC] First-class scheduled runs and cron automation #3892 "The tool should not silently create indefinite cron jobs from ambiguous language" 给出的第一种办法处理,不需要草稿状态。 - 上下文模式。 工具创建的任务默认
fresh_thread_per_run;用户要求结果回到本对话时,用当前对话的reuse_thread,此时不能带 goal(见第 2 节)。 - 范围。 工具只管理它从当前对话创建的任务。任务用一个新的可空字段记录该对话,
GET /api/threads/{id}/scheduled-tasks也返回它,因此聊天页的"定时任务"按钮能找到它。 - 工具结果。 按 [RFC] First-class scheduled runs and cron automation #3892 的要求返回任务 ID、调度、下次运行时间、结束条件和停止方法,工具描述要求智能体在回复中复述。停止方法是任务页或在对话里说一声;在 IM 渠道中是在对话里说一声。
- 在定时运行内部没有创建、列出、暂停和删除。对工具创建的任务只保留一个动作:停止我自己的调度。[RFC] First-class scheduled runs and cron automation #3892 的规则(定时运行中的智能体 "cannot recursively create schedules unless explicitly allowed")仍然成立。由于执行进行中任务定义不可修改,这个动作不在运行中改任务:它把请求记在本次运行上,由完结执行记录的代码(完成回调,或重启后的恢复函数)在同一事务里把任务改为
paused,last_error写明 "stopped by the agent in run "。不删除任何东西,用户可以恢复。 - 结束条件。 可选的
max_runs(只计定时触发的执行;手动触发,包括试跑,都不计入)和可选的截止时间;任一条件达到时任务变为completed。工具创建的调度如果触发频率高于每小时一次(interval小于 3600 秒,或 cron 的分钟字段不是单个数字),必须带其中之一。智能体从对话中取得结束条件,用户没有给出时就提问。结束条件在完结执行记录时检查,派发时再检查一次,因此截止时间已过后被恢复的任务会直接变为completed而不再运行;结束条件和下文的自动暂停在同一次执行上同时触发时,以结束条件为准。
2. 以 goal 运行
- 适用范围。 工具接受一个可选的 goal 目标描述(objective),与提示词分开:由智能体根据用户的话写成一个可检验的结果。没有它,任务就不带 goal。本 PR 中只有
fresh_thread_per_run任务可以带:在被复用的对话上,定时任务的 goal 会替换用户在那里设置的 goal,评估器的证据窗口也会跨到之前的执行。 - 生命周期。 worker 在第一轮之前,用运行 metadata 里的目标设置 goal,并在运行结束时移除,无论终态是什么。每次执行的计数都从零开始,并沿用现有上限(8 次续跑,2 次无进展)。
- 评估器。 对无法询问用户的运行(通过
RunInteractionPolicy选择,因此 webhook 和 autonomous 运行也包括在内,除非你们希望只限定时运行),上面引用的评估器关于假设的规则替换为:"This run had no user to ask. A low-risk, reversible assumption about a detail the request left open, stated in the final answer, is not by itself a reason to fail the goal. It does not replace evidence that the objective was achieved, and an assumption that is not stated still fails it." 评估器提示词同时说明:最终回答为BLOCKED结果时 blocker 取needs_user_input;这只是提示词说明,没有解析器。运行提示词要求每个假设写一行:哪里不明确、选了什么、为什么。 - 结果。 对话状态中的
GoalState不变。最终评估结论与终态在同一次写入中写进 run 行的一个新可空字段(不用stop_reason,它已承载上限和恢复原因,且可能与结论同时出现)。完成回调和恢复函数读取它。 - 新增执行记录状态
unmet: 运行结束,但 goal 未达成。goal 任务的运行以success结束但没有"已达成"的结论时,即为unmet。原因(现有的stand_down_reason;运行未经评估就结束时为no_verdict)写入执行记录现有的error字段,运行历史会显示。once任务的执行记录为unmet时,任务变为failed。unmet加入状态枚举及其终态集合、历史筛选、前端类型和文案。经 feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135 的 outbox,unmet执行有专门的通知文案并写明原因代码:run_failed的文案按设计不向 IM 发送错误细节,而原因代码是 DeerFlow 自己的词汇,不是原始错误。迁移的降级把unmet改写为failed,与 0015 降级时把launching改写为queued一样。 - 自动暂停。 周期性 goal 任务的一次定时执行以
unmet结束,且之前的两次定时执行也是unmet时,任务暂停,原因写入last_error。一次success重新计数,依赖已说明假设的success也是如此(见「修订」)。failed、interrupted、skipped的执行、手动触发,以及原因为external_wait的unmet都跳过不计。检查在完结执行记录时进行,因此恢复后的任务一定会再运行一次,但再有一次unmet就会再次暂停。本提案中对父任务的所有变更(停止请求、自动暂停、结束条件)都在complete_run内执行,且只在执行记录第一次写入终态时执行。complete_run目前并不保证这一点:对同一执行记录和运行的第二次调用会覆盖其状态并返回True,只有启动记账有保护。本 PR 会加上这项检查,因此迟到的重复完成回调不会把停止执行两次,也不会把用户已恢复的任务再次暂停。自动暂停也有专门的通知文案;按现有渲染器,除run_failed以外的事件都会显示为 "Scheduled task completed"。
3. 三项附加功能
每一项都很小,不需要前端改动,也可以单独拿掉(问题 8)。
- 引用上一次执行。 对每次新开对话的周期任务,从第二次执行起,每次都把上一次执行的对话作为只读的会话引用挂上,机制与用户 @ 一个历史对话相同([RFC] Cross-session conversation references and retrieval #5398)。运行里只多一行隐藏消息写明那个对话,
read_conversation按需读取,所有权检查同样严格,只读可见消息。用不用由智能体判断:报告可以说明相比上次有什么变化,状态检查可以和上次的状态对比,提醒类直接忽略。不需要为每个任务做配置。目前只有用户提交的运行能带引用,读取器也绑定在 HTTP 请求的权限上,所以定时启动时改用任务所有者的权限来准备它;只在read_conversation启用时生效。没有新内容可报告的运行仍会通知;让它保持安静可以作为后续工作。 - 长期备注。 用户可以在对话里让智能体为它创建的某个任务记住一件事("周报以后用 develop 分支")。工具新增的备注动作把它存到任务上,之后每次执行都会把备注附在定时提示词后面;定时提示词本来就是用户消息,所以备注的权限不会超过提示词本身。备注只在用户明确说出时添加,智能体不会自行推断;列出动作会显示备注;最多 10 条,每条 500 字符。这样一次说明过的假设,纠正一次就够了。
- 主动提出试跑。 如果创建的周期任务每次会产出用户想先看看的东西(报告、汇总、文件),智能体会提出现在先跑一次。盯状态、等条件的任务和一次性任务不提。试跑用的是现有的手动触发,就是任务页"立即运行"按钮背后的那个,所以既不计入自动暂停的连续次数,也不消耗
max_runs。
不在本 PR 内
更多的 GoalState 状态(#3834 设想了四个;main 只有一个,本提案保持不变);根据评估器的结论把任务置为 completed(第 1 节的停止动作是智能体自己的判断,唯一的自动反应是上面的自动暂停);把新对话里的结果回帖到创建它的 DeerFlow 对话(#3892 第 4 阶段列有 "SSE/thread refresh for scheduled results";经 #6135 的 IM 投递不受影响);在被复用的对话上运行 goal 任务;聊天内的确认卡片;在任务页显示或编辑 goal 和结束条件;TUI 或内嵌客户端中的 goal 评估。
前端改动仅限于 unmet 的文案和类型项。
验收
- 在真实 Gateway 上端到端:通过工具从真实对话创建任务,按时触发,在新对话中读取给定的来源,产出一个能打开且内容正确的文件;随后智能体停止自己的调度,下一次到期时不再派发。
- 停止请求到运行结束之间 Gateway 重启,停止仍然生效;重复的完成回调不会把停止执行两次。
- 只报告真正端到端验证过的渠道的通知。写入 outbox 的投递行不算用户已收到;我没有企业微信账号,PR 会写明这项检查做到了哪一步。
- 第二次执行通过引用读到第一次的结果;在对话里添加的备注,下一次执行会遵循。
- 「代价」一节所述的运行成本对照。
问题
- 立即生效,还是创建为暂停? [RFC] First-class scheduled runs and cron automation #3892 的未决问题 5:"Should conversation-created recurring schedules require explicit UI confirmation before activation?" 我倾向于立即生效:任务是用户直接要求的,工具结果里也写明了停止方法。另一个选项不需要新界面:任务创建为
paused,用户点"恢复"。 - 在定时运行内部:完全不提供定时工具,还是只允许"停止我自己的调度"? 两种情况下都不能创建。我倾向于后者,这样一次发现工作已完成的运行可以停掉自己的调度。
- 运行结束但 goal 未达成:
unmet、failed附原因,还是success加结果字段? [RFC] First-class scheduled runs and cron automation #3892 的检查清单写着 "UI never hides a failing scheduler state behind a successful-looking task row",我据此排除第三种。failed代价最小(无新状态、无降级改写、无前端改动)。我倾向unmet,因为下一步不同:failed表示有东西坏了,unmet表示运行完成了,需要看结果或调整 goal,自动暂停也要区分两者。选unmet时,feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135 的 outbox 会发一条写明原因的专门通知;选failed时,发的是通用的失败通知,按设计不含错误文本。 - 在无法询问用户的运行中,评估器可否接受最终回答里已说明的低风险假设? 我倾向于可以,措辞见第 2 节。适用于所有这类运行,还是只限定时运行?如果适用于所有这类运行,现有 webhook 和 autonomous 的 goal 运行的判定方式会改变,因此会附带升级说明和单独的测试。
- 工具创建的任务的上限(任务页和 REST API 保持现状):每个用户最多 20 个存续中的工具创建任务,存续指启用、运行中或暂停,因此暂停不会腾出名额,任务页上的恢复也不需要检查;计数和插入在同一事务中完成。另外,触发频率高于每小时一次的必须带结束条件。我倾向先用常量,而不是
scheduler.*配置。可以接受吗,还是希望用别的数字? - 周期性 goal 任务连续 3 次
unmet后自动暂停,规则见第 2 节。main上目前没有任何机制会自动暂停周期任务,这将是第一条这样的规则。可以接受吗? - 新对话里运行的任务,结果留在该对话和任务页,本 PR 不回帖到创建它的 DeerFlow 对话。IM 投递是另一回事,保持不变:feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135 会把每次定时执行通知到所有者已连接的渠道,与运行在哪个对话无关,因此从 IM 对话创建的任务仍会在那里收到结果(目前只有企业微信;其他已绑定渠道会记为投递失败)。可以接受吗,还是回帖到网页对话也应包括在内?
- 第 3 节的三项附加功能可以分开取舍。我倾向都放进第一个 PR,因为都不需要前端改动,而且正是它们让周期任务值得长期留着。你们是否希望把其中某项留到以后?
材料
- 先前的 RFC:[RFC] First-class scheduled runs and cron automation #3892(定时运行)、[RFC] Claude Code-style /goal with evaluator-driven continuation #3834(
/goal)。
修订
- 在对第一条评论的回复中接受:工具结果原文引用 goal 目标;定时运行的判定记录「判为达成是否依赖了已说明的假设」,这会显示给用户,但不改变自动暂停规则;自动暂停经 feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135 的 outbox 投递,有专门的文案并写明原因。
- 更正:
complete_run目前还不拒绝对执行记录的第二次终态写入(见「以 goal 运行」);unmet需要专门的通知文案,因为run_failed的文案不含错误细节;通知依赖改为 feat(scheduler): push scheduled task results to IM channels via a notification outbox #6135,它承接了 feat(scheduler): push scheduled task results to IM channels via a notification outbox #4843。
Activity
fujiezee commentedon Oct 1, 2026
Thanks for laying out the open questions this clearly. On question 4 (accepting stated assumptions), two small additions that might fit without new UI, since "a confirmation card in the chat" and "showing or editing the goal and end condition on the tasks page" are both listed as out of scope:
unmetentry in run history could show which criterion it was measured against, which might matter if the task is edited later.successthat rests on an accepted assumption resets the auto-pause streak, a wrong assumption could keep a task looking healthy. If those lines were also kept as a field on the occurrence (or carried in the IM message from feat(scheduler): push scheduled task results to IM channels via a notification outbox #4843), someone could scan them without opening each thread.One thing I couldn't tell from the post: if #4843 lands first,
unmetis delivered asrun_failedwith the reason, but is the auto-pause itself also delivered to the task's owner? That looks like the moment someone has to decide whether to resume, adjust the goal or drop the task.Totoro-qaq commentedon Oct 1, 2026
Thanks, all three are good catches, and none of them needs new UI.
Echo the objective. Agreed. The tool result will quote the goal objective verbatim next to the schedule and the stop instructions, and the tool description asks the agent to repeat it, so the user sees what "met" will mean and can object before the first run. The objective a run is judged against already travels with that run (the worker reads it from the run's metadata), so editing the task later cannot change what an old
unmetentry was measured against. Showing it in run history is UI work I would leave to a follow-up.A
successthat rests on an assumption. Right: an assumption that is wrong but accepted every time would keep the streak at zero and the task looking healthy. Rather than parse the answer, I would have the evaluator say it: its verdict for a scheduled run gains one boolean, whether a satisfied verdict relied on a stated assumption, stored with the verdict on the run row. The feat(scheduler): push scheduled task results to IM channels via a notification outbox #4843 message for such a run then opens with "met, relying on stated assumptions". That also covers a limit of the message itself: it keeps only the first 1,000 characters of the final answer, so an assumptions block at the end of a long answer would be cut there.Auto-pause notice. The RFC did not cover it, and it should. When the auto-pause fires, the code that finalizes the occurrence will also enqueue a
task_pauseddelivery in the feat(scheduler): push scheduled task results to IM channels via a notification outbox #4843 outbox with the reason, so the owner hears about it at the moment a decision is needed. An agent's own "stop my schedule" needs no extra message, since that run's result already goes out.I'll fold these into the PR rather than edit the RFC body again.
Totoro-qaq commentedon Oct 1, 2026
Updated the description; the seven questions are unchanged.
complete_rundoes not reject a second terminal write to an occurrence today (a repeat call overwrites the status and returnsTrue), so "only on the first terminal write" is now a requirement of this PR rather than a property ofmain. And since therun_failednotification deliberately carries no error text,unmetand the auto-pause each get their own notification text.Totoro-qaq commentedon Oct 1, 2026
Added section 3 with three small additions, each separable and none needing a frontend change: the previous occurrence attached as a read-only conversation reference (the agent decides whether reading it helps), standing notes the user adds to a task in chat, and a trial-run offer for tasks whose runs produce something to look at. With them come an eighth question (keep all three in the first PR, or leave some for later), one acceptance item, and one fix:
max_runsnow counts scheduled occurrences only, so a trial run does not use one up. Questions 1 to 7 are unchanged.