[PRD] arise register_callback(用户设到点提醒)落地 #62
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
从终端用户角度:用户经常会顺口让机器人"半小时后提醒我关火""明天早上8点提醒我交作业",但目前 arise 完全没有能力记住这类带具体触发时间的请求——它只能在当轮回答,之后就把这件事忘得一干二净,用户必须自己记着到点再来问。
从架构角度:这个缺口其实已经不该再存在了。深挖任务委托(issue #52 PRD → #53-#56)实现期顺带把 Callback 建成了一个真正通用的送回抽象——持久化、到期扫描、无门控直接送回,
trigger_source字段从一开始就是为"未来还会有别的触发源"设计的,CallbackRecord.fire_at字段也从一开始就支持非空的真实到点时间(当前唯一生产者"任务完成"用的是fire_at=None的"立即到期"这一种取值)。CONTEXT.md"回调(Callback)"一节从最初设计起就明确写着"触发源不止'到点'一种:register_callback……为其一"——地基已经等在那里,只是这个工具本身从未被实现。从维护者角度:如果这次不做,Callback 这条通用管线就永远只有一个真实生产者验证过,"通用"这个说法就只是理论上的。用一个真正独立、语义不同的第二生产者(时间到点 vs 任务完成)来验证这条管线是否真的不需要为新生产者改动,是这次实现最有价值的副产物。
Solution
新增
register_callback(content, fire_at)/cancel_callback(callback_id)两个 core 原生工具(同delegate_task/cancel_delegate_task先例,不经工具港)。模型识别到用户请求一个带具体触发时间的提醒时,自己基于当前消息时间戳推算出目标时间(ISO8601),调用register_callback写入一条trigger_source="user_reminder"的 Callback 记录;到点后由已有的、零改动的_callback_scan/run_callback()管线无门控送回,模型用平时语气自然说出来。用户改主意时,模型调用cancel_callback撤销——为了让这个取消在几天后依然可用,新增一段纯函数渲染,把当前 chat 待定的user_reminder记录(含 id)纳入冻结快照,供模型跨轮次引用。User Stories
用户视角:设提醒
用户视角:取消提醒
用户视角:边界与信任
Host 部署者视角
维护者/贡献者视角
fire_at是不是已经过去了"这类校验逻辑本身是纯函数,能直接构造固定输入断言,不需要真实时钟。register_callback/cancel_callback在RuntimeLoop里的测试方式跟现有delegate_task/cancel_delegate_task一样,直接喂脚本化对话断言效果,不需要真的等待到点或起后台任务。_callback_scan/_deliver_callback/run_callback()/is_due/render_callback_directive这几个既有模块做任何改动——如果实现期发现真的需要改这几个文件,这本身就是一个值得先停下核实"通用抽象是不是真的通用"的信号。验收标准视角
register_callback产出的记录trigger_source取值明确区别于深挖任务委托的task_completion,cancel_callback只会撤销匹配来源/id 的那一条,不会误伤深挖任务委托正在待送回的记录。fire_at到点触发(受限于既有扫描间隔的粒度,不要求秒级精确)。delegate_task先例:用户显式请求触发的功能,不适用"新部署更保守"的理由。Implementation Decisions
新增核心工具
register_callback(content, fire_at):core 原生工具(同delegate_task/note_pending_intent先例,不经工具港,不新增能力位)。fire_at是 ISO8601 时间字符串,由模型自己基于当前消息真实时间戳推算得出——前处理 port 已经把真实时间戳带进每条消息(ADR-0018),前台工具调用有活的推理能力,不新增一层类似 Delta 压缩管线那种确定性正则时间解析(那是异步批处理场景的专属方案,这里不适用)。fire_at解析失败,或早于/等于当前时间——拒绝,工具结果返回一句可读错误交给模型组织语言告知用户,不静默创建一条永远不会触发的记录,也不做"帮用户纠正成明天这个点"式的猜测。cancel_callback(callback_id):core 原生工具。callback_id来自下述"待定提醒渲染"——模型从渲染文本里读到具体 id 再引用。找不到匹配记录(id 不存在,或存在但不属于user_reminder来源)——fail-closed 无操作,不报错阻断对话,同cancel_delegate_task先例。trigger_source常量(如USER_REMINDER_TRIGGER_SOURCE = "user_reminder"),仿delegate_task.py::TASK_COMPLETION_TRIGGER_SOURCE先例——各生产者自己持有自己的常量,callback.py本身不维护生产者清单。取消时只按这个来源过滤,不触碰深挖任务委托的待送回记录(同 issue #56 已建立的"不误删其它来源"先例)。RUN_LIMIT_EXEMPT_TOOL_NAMES(登记/撤销一次提醒不是当轮"推理",同delegate_task/cancel_delegate_task归类理由)。待定提醒渲染(新增,唯一需要往复用管线里"加一段"的地方)
pending_intent.py::render_pending_intents先例),把当前 chat 的待定user_reminder记录渲染进冻结快照——挂进_full_frozen_snapshot()既有六段拼装之后的新增第七段,只在存在待定记录时产出内容(同其余快照分段"无数据即空字符串"的一贯风格),内容含callback_id(供cancel_callback引用)、内容摘要、fire_at。trigger_source="user_reminder"的记录——task_completion来源的待送回记录不在这段渲染范围内:那类记录生命周期通常是"当轮登记、后台跑完、下一次扫描前送回",跨度短,用户中途不需要查询/取消进度(取消深挖任务走既有cancel_delegate_task,走的是任务登记表而不是这条渲染)。这个区分是本次的一个明确判断,不是"新触发源=自动也要渲染"的机械推广。反滥用静态上限
arise_max_pending_callbacks_per_chat),限制单 chat 同时存在的user_reminder待定记录数——同仓库既有"阈值三层分类"静态 config 层惯例(MAX_SENDS_PER_TURN/MAX_KNOWLEDGE_PER_TURN/MAX_EDITS_PER_MESSAGE同类先例)。超限时register_callback返回错误提示,不静默丢弃、不静默覆盖最旧的一条。明确不改动的既有管线(防止实现期误以为需要碰这些)
CallbackRecord/CallbackStoragePort/is_due/render_callback_directive(callback.py):零改动,fire_at字段本就为非空真实到点时间预留。_callback_scan/_deliver_callback(调度驱动):零改动,已经是"扫全体待定记录、按is_due过滤、忙时 defer"的通用实现,不认识任何具体trigger_source。RuntimeLoop.run_callback():零改动,履约送回统一走这一个入口,复用完整六段冻结快照 + 独立合成指令的既有组装方式。Testing Decisions
好的测试只测外部可观察行为(工具调用产生的记录状态、送回内容/时机、取消的最终效果),不测内部实现细节。
register_callback/cancel_callback的参数校验(过去时间拒绝、格式错误拒绝)、待定提醒渲染(多条记录/零记录空字符串/字段完整性),仿pending_intent.py既有测试模式。register_callback/cancel_callback直接喂RuntimeLoop.run()脚本化对话断言效果(登记成功返回 id、超限拒绝、取消成功、取消不存在的 id fail-closed、跨 chat 隔离),仿现有test_delegate_task_tools.py模式。register_callback登记的记录能被既有_callback_scan/run_callback()不加任何改动地正确送回——仿test_callback_scan_e2e.py已有的端到端模式,换一个生产者、走同一条管线。这条测试本身就是"Callback 抽象真的通用"这个论断的证据。cancel_callback只撤销user_reminder来源记录,不误伤同一 chat 下task_completion来源的待送回记录(构造两者同时存在的场景),仿 issue #56 已验证过的"不误删其它来源"测试先例。user_reminder记录原样还在(同 issue #56 已验证的 Callback 持久化不变量,这次换一个生产者再验一遍,防止未来有人把某个生产者的记录错误地做成非持久化)。Out of Scope
_callback_scan/_deliver_callback/run_callback()/is_due/render_callback_directive)——本次是验证这条管线设计时就预留好的"新生产者只需新增,不改地基",不是重新设计它。register_callback的既定定义是"纯 time 模式"的一次性到点,周期语义涉及时区/日历处理,是完全不同量级的功能,不在本次范围。fire_at,不建类似 Delta 压缩管线那种正则解析层,见 Implementation Decisions。/cost//why//why_query//diagnose、六池成本治理)、习得贴纸(ADR-0024)、跨平台资料拉取(ADR-0027)、记忆深度搜索(ADR-0031)、mood_congruence(ADR-0009 更新)、环境遥测(ADR-0014 更新):均为独立条目,不在本次顺带实现。Further Notes
callback.py/_callback_scan本身,应该先停下来核实是不是通用抽象本身有设计缺陷,而不是想当然地加trigger_source特判分支糊过去。task_completion不需要渲染、user_reminder需要,理由是两者生命周期跨度不同。如果未来 Callback 出现第三个触发源,需要重新做一次同样的判断,不能假设"新触发源=自动也要渲染进上下文"。arise_max_pending_callbacks_per_chat的具体数字,同仓库既有"阈值三层分类"原则(ADR-0011),推迟到真实使用数据标定,本 PRD 不预先钉死。