feat: delegate_task/cancel_delegate_task 前台工具(issue #55) #59

Merged
Yushu merged 2 commits from feature/55-delegate-task-tools into main 2026-07-24 07:23:43 +00:00
Member

Closes #55

这个 PR 做了什么

实现 issue #55「delegate_task/cancel_delegate_task 前台工具 + 任务登记表」。这是深挖任务委托(issue #52 PRD)的前台触发入口——用户显式请求 → 模型登记任务 → 当轮应声 → 后台异步跑 #54 的执行核。本片暂不接完成后的送回(issue #56 的范围)。

进程内任务登记表(delegate_task.py

  • DelegatedTaskRegistry:per-chat_id 至多一条运行中任务句柄,内存字典不落盘(ADR-0020"备注"节已拍板,同 _pending_flushes/_pending_followup_checks 先例)——进程崩溃/重启即丢失在途任务,不重放
  • 须跨多次 RuntimeLoop 触发持久存在(同 storage/_silence_window 先例):__init__.py 构造一个进程级单例,经 RuntimeLoop 构造函数注入

两个新增 core 原生工具(tools.py

  • delegate_task(description):不经工具港(同 note_pending_intent/search_memory 先例)
  • cancel_delegate_task(task_id?)
  • 恒可见,不受渐进解锁门控(ADR-0020:深挖任务委托是用户显式请求触发,不适用"新部署更保守"的理由)
  • 两者加入 RUN_LIMIT_EXEMPT_TOOL_NAMES(原 OUTPUT_TOOL_NAMES,本次改名——不计入本轮 run_limit,ADR-0020"本身不是当轮消耗的一步推理,真正的推理工作在后台异步跑")

RuntimeLoop 工具处理器

  • _handle_delegate_task:先查登记表判重——已有任务在跑时直接返回可读的"已有任务在跑"结果,不新起第二个;否则 asyncio.create_task 启动 #54 的执行核并登记,不阻塞当轮
  • _handle_cancel_delegate_task:真中断(task.cancel()),找不到对应任务(或 task_id 不匹配)视为无操作,fail-closed 不报错阻断对话
  • _run_delegated_task:后台任务包装,收尾(正常完成或被取消)后从登记表移除自己

测试

  • test_delegate_task.py:登记表纯单测,含真实 asyncio.Task 取消语义验证
  • test_delegate_task_tools.pyRuntimeLoop.run() 脚本化对话测试——登记+同轮应声、去重、跨 chat 隔离、run_limit 豁免、真中断、fail-closed、端到端验证登记的任务确实驱动 #54 执行核(显式 await 任务让其在受控脚本下真正跑完)

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

Code review

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

  • Spec 轴:6 条 AC 全部落实,未发现越界,未发现遗漏
  • Standards 轴发现两处判断性问题,均已修复:① OUTPUT_TOOL_NAMES 语义已超出"输出类工具"范畴(新增两个工具豁免 run_limit 的理由不同),改名为 RUN_LIMIT_EXEMPT_TOOL_NAMES;② _run_delegated_tasktry/finally 缺少异常可观测性说明,补充文档解释为何不加 except(模块刻意框架无关,不依赖 nonebot.log,同既有 fire-and-forget 任务一致的已接受风险敞口)
Closes #55 ## 这个 PR 做了什么 实现 issue #55「delegate_task/cancel_delegate_task 前台工具 + 任务登记表」。这是深挖任务委托(issue #52 PRD)的前台触发入口——用户显式请求 → 模型登记任务 → 当轮应声 → 后台异步跑 #54 的执行核。本片暂不接完成后的送回(issue #56 的范围)。 ### 进程内任务登记表(`delegate_task.py`) - `DelegatedTaskRegistry`:per-chat_id 至多一条运行中任务句柄,内存字典不落盘(ADR-0020"备注"节已拍板,同 `_pending_flushes`/`_pending_followup_checks` 先例)——进程崩溃/重启即丢失在途任务,不重放 - 须跨多次 `RuntimeLoop` 触发持久存在(同 `storage`/`_silence_window` 先例):`__init__.py` 构造一个进程级单例,经 `RuntimeLoop` 构造函数注入 ### 两个新增 core 原生工具(`tools.py`) - `delegate_task(description)`:不经工具港(同 `note_pending_intent`/`search_memory` 先例) - `cancel_delegate_task(task_id?)` - 恒可见,不受渐进解锁门控(ADR-0020:深挖任务委托是用户显式请求触发,不适用"新部署更保守"的理由) - 两者加入 `RUN_LIMIT_EXEMPT_TOOL_NAMES`(原 `OUTPUT_TOOL_NAMES`,本次改名——不计入本轮 `run_limit`,ADR-0020"本身不是当轮消耗的一步推理,真正的推理工作在后台异步跑") ### RuntimeLoop 工具处理器 - `_handle_delegate_task`:先查登记表判重——已有任务在跑时直接返回可读的"已有任务在跑"结果,不新起第二个;否则 `asyncio.create_task` 启动 #54 的执行核并登记,不阻塞当轮 - `_handle_cancel_delegate_task`:真中断(`task.cancel()`),找不到对应任务(或 `task_id` 不匹配)视为无操作,fail-closed 不报错阻断对话 - `_run_delegated_task`:后台任务包装,收尾(正常完成或被取消)后从登记表移除自己 ### 测试 - `test_delegate_task.py`:登记表纯单测,含真实 `asyncio.Task` 取消语义验证 - `test_delegate_task_tools.py`:`RuntimeLoop.run()` 脚本化对话测试——登记+同轮应声、去重、跨 chat 隔离、`run_limit` 豁免、真中断、fail-closed、端到端验证登记的任务确实驱动 #54 执行核(显式 `await` 任务让其在受控脚本下真正跑完) 全部 878 个测试通过,`ruff check`/`ruff format` 干净。 ### Code review 跑过 `/code-review`(Standards + Spec 两轴并行子智能体): - Spec 轴:6 条 AC 全部落实,未发现越界,未发现遗漏 - Standards 轴发现两处判断性问题,均已修复:① `OUTPUT_TOOL_NAMES` 语义已超出"输出类工具"范畴(新增两个工具豁免 run_limit 的理由不同),改名为 `RUN_LIMIT_EXEMPT_TOOL_NAMES`;② `_run_delegated_task` 的 `try/finally` 缺少异常可观测性说明,补充文档解释为何不加 `except`(模块刻意框架无关,不依赖 `nonebot.log`,同既有 fire-and-forget 任务一致的已接受风险敞口)
实现 issue #55「delegate_task/cancel_delegate_task 前台工具 + 任务登记表」
——深挖任务委托(issue #52 PRD)的前台触发入口。用户显式请求→模型登记
任务→当轮应声→后台异步跑 #54 的执行核,本片暂不接完成后的送回(issue
#56 的范围)。

- 新增 delegate_task.py:DelegatedTaskRegistry 进程内登记表(per-chat
  至多一条运行中任务句柄,内存字典不落盘,同 _pending_flushes 先例)
- tools.py:新增 delegate_task/cancel_delegate_task 两个 core 原生工具
  schema,恒可见(不受渐进解锁门控);两者加入 OUTPUT_TOOL_NAMES 豁免
  run_limit 计数(ADR-0020"不计入本轮 run_limit")
- runtime_loop.py:RuntimeLoop 新增 delegate_task_registry/
  delegate_max_rounds 构造参数(须跨多次触发持久存在,同
  storage/_silence_window 先例,经 __init__.py 单例注入);两个新工具
  handler——delegate_task 去重检查+asyncio.create_task 启动#54执行核;
  cancel_delegate_task 真中断(task.cancel())+ fail-closed 找不到即
  无操作
- __init__.py:模块级 _delegate_task_registry 单例,接入
  _build_runtime_loop

测试:登记表纯单测(含真实 asyncio.Task 取消语义)、RuntimeLoop 级
ScriptedLLMClient 脚本化对话测试(登记+应声+去重+run_limit豁免+真中断+
fail-closed+端到端验证登记的任务确实驱动 #54 执行核)。

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- tools.py/runtime_loop.py/tests:OUTPUT_TOOL_NAMES 改名为
  RUN_LIMIT_EXEMPT_TOOL_NAMES——issue #55 起这个集合已不再是纯"输出类
  工具"的同义词(新增的 delegate_task/cancel_delegate_task 豁免理由是
  ADR-0020"不计入 run_limit",跟 ADR-0012"输出动作不占份额"是两种不同
  的豁免理由),改名反映这一更宽的含义,避免误导未来按名字理解的人
- runtime_loop.py:_run_delegated_task 的 try/finally 补充文档说明为何
  不带 except——run_delegated_task 内部已 fail-closed,call_host_tool
  也吞掉工具异常,残余异常面很窄;本模块刻意不依赖 nonebot.log(框架
  无关,同 reflection.py/subagent.py 等既有先例),万一真触发交给
  asyncio 默认处理器浮现,同 __init__.py 现有 fire-and-forget 任务
  一致的已接受风险敞口,不是新引入的缺口

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit 625c03cab4 into main 2026-07-24 07:23:43 +00:00
Yushu deleted branch feature/55-delegate-task-tools 2026-07-24 07:23:44 +00:00
Sign in to join this conversation.
No description provided.