feat: Callback 送回抽象(issue #53) #57

Merged
Yushu merged 2 commits from feature/53-callback-return-abstraction into main 2026-07-24 06:05:14 +00:00
Member

Closes #53

这个 PR 做了什么

实现 issue #53「Callback 送回抽象:存储 + 到期扫描 + 无门控送回集成」。这是深挖任务委托(issue #52 PRD)"任务完成后无门控送回"投递语义的地基——本片只建通用管线本身,不接任何真实生产者(issue #54/#55/#56 均在此片之后,测试直接写入记录来验证行为)。

新增 Callback 存储切片(callback.py

  • CallbackRecordid/chat_id/content/trigger_source(当前只有 "task_completion" 一个取值,字段本身对未来扩展开放,如 register_callback"user_reminder")/created_at/fire_at(可空——本次唯一生产者语义上是"立即到期")
  • is_due:到期判定纯函数(fire_at 为空或已到期即算到期),零依赖真实时钟/调度器
  • render_callback_directive:送回时追加的合成 system 指令——措辞刻意保持生产者中立,不假设任何具体生产者的产出形态
  • CallbackStoragePort:per-chat_id 支持多条待定记录,不是单槽位设计(同 pending_intent.py 的 per-scope 多记录先例)
  • sent_log.py/storage.py:内存版(SentLog)+ Postgres/SQLite 持久化版(PersistentStorage)双实现

RuntimeLoop.run_callback()runtime_loop.py

  • run() 抽出 _full_frozen_snapshot() 私有方法(画像 + 三因子召回 + 关系网 + 距上次交互 + 未决意图 + 输入状态六段全量拼装),run_callback() 复用同一套——区别于 run_light()/_run_event_driven_turn() 的裁剪版(issue #53 AC 明确要求"而不是走 run_light/事件驱动的裁剪版组装")
  • 抽出 _run_directive_turn() 共用尾段(assemble + 追加合成指令 + 工具循环 + flush),run_callback() 与既有 _run_event_driven_turn() 共用,避免重复实现
  • 不写 Delta Buffer(与用户核实的设计决定:履约本身不是一次新的用户互动,同 run_light() 既有边界);不写 familiarity/距上次交互重置(没有真实消息可归因)
  • 私聊场景 sender_ids 取对方裸 user_id(同 Drive Tick 先例);群聊没有单一可归因用户(CallbackRecord 不携带触发者 user_id),保持空

__init__.py 调度驱动

  • 新增 _callback_scan APScheduler 定时任务(arise_callback_scan_interval_seconds,默认 30s),扫描全体待定记录,到期即经 _deliver_callback 无门控直接送回
  • 忙时 defer:复用既有 _pending_flushes 追踪(同 Drive Tick 先例),正被前台占用的 chat 顺延到下一次扫描——记录不删除,天然重试
  • 不调用 _evaluate_unified_gate,不消耗两级门控的沉默预算(issue #53 AC)
  • 送回成功后 delete_callback,不会被下一次扫描重复送回

测试

新增 4 个测试文件:纯函数(is_due/render_callback_directive)、存储契约(内存版 + 持久化版共用,含 per-chat 多条不互相覆盖)、RuntimeLoop.run_callback() 的完整快照区分度断言(私聊/群聊)、_callback_scan 端到端场景(零 LLM 调用/送达/不重发/忙时 defer/门控绕过 + 沉默预算不被触碰)。

全部 841 个测试通过,ruff check/ruff format 干净。

Code review

跑过 /code-review(Standards + Spec 两轴并行子智能体):

  • Spec 轴发现 render_callback_directive 措辞越界(把 issue #56 专属的"不要写成分析报告"钉死进了本应生产者中立的通用模块)——已修复,泛化为"不要机械照搬原文"
  • Standards 轴发现 run_callback 与既有 _run_event_driven_turn 尾段重复——已抽出 _run_directive_turn 共用
  • 均已在第二个 commit 里修复
Closes #53 ## 这个 PR 做了什么 实现 issue #53「Callback 送回抽象:存储 + 到期扫描 + 无门控送回集成」。这是深挖任务委托(issue #52 PRD)"任务完成后无门控送回"投递语义的地基——本片只建通用管线本身,不接任何真实生产者(issue #54/#55/#56 均在此片之后,测试直接写入记录来验证行为)。 ### 新增 Callback 存储切片(`callback.py`) - `CallbackRecord`:`id`/`chat_id`/`content`/`trigger_source`(当前只有 `"task_completion"` 一个取值,字段本身对未来扩展开放,如 `register_callback` 的 `"user_reminder"`)/`created_at`/`fire_at`(可空——本次唯一生产者语义上是"立即到期") - `is_due`:到期判定纯函数(`fire_at` 为空或已到期即算到期),零依赖真实时钟/调度器 - `render_callback_directive`:送回时追加的合成 system 指令——措辞刻意保持生产者中立,不假设任何具体生产者的产出形态 - `CallbackStoragePort`:per-chat_id 支持多条待定记录,不是单槽位设计(同 `pending_intent.py` 的 per-scope 多记录先例) - `sent_log.py`/`storage.py`:内存版(`SentLog`)+ Postgres/SQLite 持久化版(`PersistentStorage`)双实现 ### `RuntimeLoop.run_callback()`(`runtime_loop.py`) - 从 `run()` 抽出 `_full_frozen_snapshot()` 私有方法(画像 + 三因子召回 + 关系网 + 距上次交互 + 未决意图 + 输入状态六段全量拼装),`run_callback()` 复用同一套——区别于 `run_light()`/`_run_event_driven_turn()` 的裁剪版(issue #53 AC 明确要求"而不是走 run_light/事件驱动的裁剪版组装") - 抽出 `_run_directive_turn()` 共用尾段(assemble + 追加合成指令 + 工具循环 + flush),`run_callback()` 与既有 `_run_event_driven_turn()` 共用,避免重复实现 - 不写 Delta Buffer(与用户核实的设计决定:履约本身不是一次新的用户互动,同 `run_light()` 既有边界);不写 familiarity/距上次交互重置(没有真实消息可归因) - 私聊场景 `sender_ids` 取对方裸 user_id(同 Drive Tick 先例);群聊没有单一可归因用户(`CallbackRecord` 不携带触发者 user_id),保持空 ### `__init__.py` 调度驱动 - 新增 `_callback_scan` APScheduler 定时任务(`arise_callback_scan_interval_seconds`,默认 30s),扫描全体待定记录,到期即经 `_deliver_callback` 无门控直接送回 - 忙时 defer:复用既有 `_pending_flushes` 追踪(同 Drive Tick 先例),正被前台占用的 chat 顺延到下一次扫描——记录不删除,天然重试 - 不调用 `_evaluate_unified_gate`,不消耗两级门控的沉默预算(issue #53 AC) - 送回成功后 `delete_callback`,不会被下一次扫描重复送回 ### 测试 新增 4 个测试文件:纯函数(`is_due`/`render_callback_directive`)、存储契约(内存版 + 持久化版共用,含 per-chat 多条不互相覆盖)、`RuntimeLoop.run_callback()` 的完整快照区分度断言(私聊/群聊)、`_callback_scan` 端到端场景(零 LLM 调用/送达/不重发/忙时 defer/门控绕过 + 沉默预算不被触碰)。 全部 841 个测试通过,`ruff check`/`ruff format` 干净。 ### Code review 跑过 `/code-review`(Standards + Spec 两轴并行子智能体): - Spec 轴发现 `render_callback_directive` 措辞越界(把 issue #56 专属的"不要写成分析报告"钉死进了本应生产者中立的通用模块)——已修复,泛化为"不要机械照搬原文" - Standards 轴发现 `run_callback` 与既有 `_run_event_driven_turn` 尾段重复——已抽出 `_run_directive_turn` 共用 - 均已在第二个 commit 里修复
实现 issue #53「Callback 送回抽象:存储+到期扫描+无门控送回集成」——
深挖任务委托(issue #52 PRD)"任务完成后无门控送回"投递语义的地基,
本片只建通用管线本身,不接任何真实生产者(测试直接写入记录验证)。

- 新增 callback.py:CallbackRecord(id/chat_id/content/trigger_source/
  created_at/fire_at)、is_due 纯函数(fire_at 为空或已到期即算到期)、
  render_callback_directive(送回时的合成 system 指令)、
  CallbackStoragePort(per-chat 多条待定,同 pending_intent 先例)
- sent_log.py/storage.py:内存版 + Postgres/SQLite 持久化版双实现
- runtime_loop.py:从 run() 抽出 _full_frozen_snapshot 私有方法(画像+
  召回+关系网+距上次交互+未决意图+输入状态六段全量拼装),新增
  run_callback() 复用同一套完整快照——区别于 run_light()/事件驱动路径
  的裁剪版;不写 Delta(与用户核实:履约本身不是新的用户互动)
- __init__.py:新增 _callback_scan APScheduler 定时任务,扫描到期记录,
  忙时 defer(复用既有 _pending_flushes 追踪),不经过两级门控/不消耗
  沉默预算

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- callback.py:render_callback_directive 措辞泛化——去掉"不要写成分析
  报告/不要罗列原始数据"这类深挖任务委托(issue #56)专属措辞,本模块
  声明的"生产者中立"边界不该被任何单一生产者的产出形态假设污染
- runtime_loop.py:抽出 _run_directive_turn 共用尾段(assemble+追加合成
  指令+工具循环+flush),run_callback() 与既有 _run_event_driven_turn
  共用,消除两处形状相同的重复代码

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit 80ceb7c916 into main 2026-07-24 06:05:14 +00:00
Yushu deleted branch feature/53-callback-return-abstraction 2026-07-24 06:05:15 +00:00
Sign in to join this conversation.
No description provided.