[PRD] arise 深挖任务委托(Deep Task Delegation)落地 #52

Closed
opened 2026-07-24 04:00:02 +00:00 by KumaAgent · 0 comments
Member

Problem Statement

从终端用户角度:现在的 arise 只能在当轮 run_limit(默认 6 步推理工具调用)内直接给出回答。一旦用户提的问题需要"钻研"——比如"帮我分析总结一下我这千把条舞萌成绩打得怎么样"——这类任务原始数据量太大,不能直接倒进当轮 prompt(会撑爆上下文、破坏冻结快照的 prefix-cache),分析所需的步骤也太多、太多样,撑不满一个前台短循环。目前 arise 对这类请求要么勉强在 run_limit 内做浅层分析敷衍过去,要么干脆做不了。

从 host 部署者角度:CONTEXT.md/design.md 已经把"深挖任务委托"作为既定架构决策写进了文档(ADR-0020),但仓库里没有任何代码实现,也没有它依赖的"无门控送回"机制(Callback)。这个缺口挡在"能不能让 arise 真正处理复杂请求"和"现在只能处理三言两语能说清的问题"之间。

从维护者角度:ADR-0020 明确要求这类后台任务"异步执行、带工具、隔离任务上下文、不直接对用户说话",如果不从设计上把这几条约束落实为可测的边界,后续任何子智能体相关的改动都可能不小心开一条绕过人设/情感态调节的发言通道,或者让后台任务偷偷占用前台的成本/并发预算。

Solution

实现 ADR-0020 定义的深挖任务委托机制:主模型识别到用户请求需要深挖时,调用新增核心工具 delegate_task 登记一个异步后台任务、紧跟一次 send_message 应一声("我看看啊")、当轮结束——不占用前台 run_limit/并发槽位。后台一个隔离上下文的子智能体(只带任务描述 + 工具港注册的 host 工具,无人设/记忆/关系/情感态/历史)自主完成多轮工具调用钻研数据,产出蒸馏文本结果。任务完成后,结果作为普通内容"无门控"重新进入一次带全套人设/记忆上下文的正常 Runtime Loop,由主模型自己组织语气、调 send_message 发出——子智能体全程不直接对用户说话。用户可用自然语言("算了不用查了")触发 cancel_delegate_task,真中断底层任务。

由于仓库里承接"任务完成后无门控重新进入会话"这个投递语义的 Callback 机制此前完全不存在(pending_intent.py 现有实现明确只覆盖 context 模式,time 模式/Callback 从未落地),本次顺带把 Callback 建成一个通用抽象(持久化、per-chat 多条待定、无门控直接送回),深挖任务完成是它的第一个真实触发源。register_callback(用户主动设置到点提醒)这个未来触发源本身不在本次范围内——只是让这次新建的存储/送回抽象足够通用,将来加它时只需要新增一个工具 + 一个到点扫描生产者,不需要改动本次建的地基。

User Stories

私聊/群聊用户视角:请求深挖

  1. 作为用户,我希望问机器人一个明显需要翻很多数据才能回答的问题(如"分析总结一下我这些成绩")时,它会先应一声"我看看啊"之类的话,而不是让我干等着不知道它有没有收到。
  2. 作为用户,我希望机器人应声之后,我可以继续正常聊别的,不需要在原地等它,聊天不会被这个深挖任务卡住。
  3. 作为用户,我希望深挖任务做完后,机器人是用它平时说话的语气把结论讲给我听,而不是甩给我一份格式化的分析报告或者原始数据转储。
  4. 作为用户,我希望得到的结论是真的经过分析总结的,不是机器人不看数据就编出来的敷衍话。
  5. 作为用户,我如果中途改主意了("算了不用查了"),我希望机器人能听懂并真的停下这个后台任务,而不是嘴上说"好的"但后台还在跑、过一会儿突然又把结果发给我。
  6. 作为用户,我希望同一个对话里一次只有一个深挖任务在跑——如果我在前一个还没做完时又提了一个新的深挖请求,机器人会告诉我"之前那个还在弄",而不是同时开好几个把系统拖垮。
  7. 作为私聊用户,我希望深挖任务的结果送回到我们这个私聊,不会送错地方。
  8. 作为群聊用户,我希望深挖任务是我在这个群里请求的,结果也送回这个群,即使这段时间群里其它人也在跟机器人聊别的。
  9. 作为用户,我希望深挖任务做完、结果送回来的这次发言,跟机器人平时主动找我说话(Recall/自发目标)不是一回事——这次是我请求过的,机器人不会因为"沉默预算"不够或"现在不是合适时机"就把结果压下不发。

Host 部署者/运维者视角:安全与可控

  1. 作为部署者,我希望后台子智能体只能调用我通过工具港注册给它的那些工具,不会被额外授予任何我没注册过的能力。
  2. 作为部署者,我希望后台子智能体不能直接绕过我配置的 persona/情感态调节直接给用户发消息——所有对外发言都经过正常的、带人设的主循环。
  3. 作为部署者,我希望深挖任务的后台工具调用轮次有一个静态硬顶,不会因为子智能体陷入死循环或数据异常复杂而无限跑下去、无限消耗 token。
  4. 作为部署者,我希望进程重启时,正在跑的深挖任务直接丢失(不重放),这跟 arise 现有的崩溃恢复哲学(丢弃在途请求)一致,不会留下奇怪的残留状态或重复送达。
  5. 作为部署者,我希望不同 chat 的深挖任务互不影响并发上限——A 群有一个任务在跑不会挡住 B 群发起自己的深挖任务。
  6. 作为部署者,我希望这次新建的送回机制(Callback)以后能直接拿来支撑"用户设置到点提醒"这类需求,不用推倒重来。

维护者/贡献者视角:可测性

  1. 作为维护者,我希望"给子智能体组装隔离任务上下文(任务描述+工具,不含人设/记忆/历史)"是一个不需要真实 LLM 调用就能直接断言其内容的纯函数。
  2. 作为维护者,我希望子智能体的多轮工具调用循环可以用脚本化的假 LLM 客户端(复用现有 ScriptedLLMClient 先例)驱动,断言"给定一串工具调用/返回,最终产出预期的蒸馏文本",不需要真实网络请求。
  3. 作为维护者,我希望 delegate_task/cancel_delegate_task 这两个新工具在 RuntimeLoop 里的处理逻辑,跟现有 note_pending_intent 等工具处理器一样,可以直接喂 RuntimeLoop.run() 一段脚本化对话来测试,不需要真的启动后台任务等待完成。
  4. 作为维护者,我希望 Callback 存储层(新增/查询/删除待定记录、per-chat 多条不互相覆盖)有独立于调度驱动的存储集成测试,同 pending_intent 现有的纯函数测试 + 存储集成测试两层先例。
  5. 作为维护者,我希望"哪些待定 Callback 现在算到期该送回"这个判定是一个不依赖真实时钟/调度器的纯函数,可以直接构造固定输入断言结果。
  6. 作为维护者,我希望子智能体工具调用失败时,这次失败被当作一次普通的失败 tool_response 处理(同工具港既有的"不自动重试"哲学),不会让整个后台任务未经处理地崩溃、也不会让调用方看不到任何反馈就静默卡死。

验收标准视角

  1. 作为验收标准,我希望后台深挖任务的工具调用轮次不计入触发它的那一轮前台 run_limit
  2. 作为验收标准,我希望子智能体被赋予的工具集里不包含 send_message/self_recall/edit 等任何输出类工具——结构上就不可能绕开主循环直接发言。
  3. 作为验收标准,我希望蒸馏结果送回时经过的是完整的 assemble_context()(安全基线 + persona + 冻结记忆快照 + 触发注入),不是一个阉割版上下文。
  4. 作为验收标准,我希望 cancel_delegate_task 在任务已经完成、结果还没来得及送回这个窄时间窗口内被调用时,也能确实压下这次送回,不会出现"说了算了、结果还是发过来了"的情况。
  5. 作为验收标准,我希望 Callback 的送回不占用/不消耗两级门控的沉默预算——它是履约通道,不是主动性决策通道。
  6. 作为验收标准,我希望新建的 Callback 存储结构支持一个 chat 同时存在多条待定记录,不会因为深挖任务完成通知占用了唯一槽位而挤掉其它待定项(即便本次没有其它生产者,也不应该是单槽位设计)。

Implementation Decisions

新增核心工具(RuntimeLoop 层,前台)

  • delegate_task(description):core 原生工具(同 note_pending_intent/search_memory 先例,非工具港 host 工具),当轮调用即在进程内登记一个后台任务(chat_id/task_id → 运行中任务句柄),随即以 asyncio 方式启动后台执行,不阻塞当轮工具循环,也不计入本轮 run_limit/MAX_SENDS_PER_TURN(本身不是 send)。模型通常紧跟一次 send_message 应声,当轮随后自然结束。
  • cancel_delegate_task(task_id?):core 原生工具,模型在对话中识别到用户要求取消时调用。若对应任务仍在跑,真中断底层 asyncio 任务;若任务已完成但结果尚未送回(见下 Callback 窗口),一并撤销这条待送回记录。找不到对应任务/已经送达过的任务,视为无操作(fail-closed,不报错阻断对话)。
  • 每个 chat 同时至多一个深挖任务在跑(进程内按 chat_id 检查);重复请求时 delegate_task 直接返回一个可读的"已有任务在跑"结果给模型,由模型组织语言告知用户,不新起第二个任务。
  • 这两个工具的可见性不受渐进解锁门控(不同于自发目标)——深挖任务是用户显式请求触发,不是机器人自主决定的主动性功能,不适用渐进解锁"新部署更保守"的理由。

任务登记(进程内存态,不持久化)

  • chat_id → 运行中任务句柄(asyncio.Task + task_id)的登记表是进程内存字典,同现有 _pending_flushes/_pending_followup_checks 先例,不落盘。进程崩溃/重启即丢失在途任务,不重放——与 ADR-0013 崩溃恢复语义一致,不是本次新开的例外。

后台子智能体执行(新模块,隔离上下文)

  • 新增一个独立模块(同 reflection.py/persona_drift_guard.py 的既有分层先例),职责是:① 组装隔离任务上下文(纯函数:只含任务描述 + 安全基线,不含 persona/画像/关系网/情感态/历史对话);② 驱动一个独立的多轮工具调用循环,使用 ports.llm_client(主力模型——当前仓库还没有独立配置的 auxiliary 模型客户端,reflection.py 现状也是复用同一个 llm_client,故这里天然满足 ADR-0020"用主力模型"的要求,不需要新增模型路由);③ 循环终止条件是子智能体自己表示"分析完毕"(不再调用工具,给出最终文本),产出该文本作为蒸馏结果。
  • 子智能体可见工具集 = 工具港 get_tools("background") 返回的 host 工具(RuntimeContext.background 这个既有契约取值首次被真正接入调用点)。子智能体工具集里不包含 core 自己的任何输出类工具(send_message/self_recall/edit/delegate_task 本身等)——结构性排除,不是运行时判断。
  • 后台工具调用轮次设独立静态硬顶(新增 config 项,不复用前台 arise_run_limit——量级不同,具体数字按 ADR-0011 既定原则推迟到真实数据标定),超过硬顶即强制结束循环,把已经拿到的内容作为蒸馏结果(不是直接判定失败——尽量拿现有信息交差,同 ADR-0028"不阻塞回复"的一贯风格)。
  • 子智能体工具调用失败按工具港既有哲学处理(失败即一次普通 tool_response,不自动重试),不中断整个后台任务。

Callback(通用送回抽象,新建)

  • 新增存储 port 切片:持久化的 Callback 记录,字段含 id、chat_id、content(送回时要注入的内容)、trigger_source(本次只有 "task_completion" 一个取值,字段本身开放给未来扩展如 "user_reminder")、created_at、fire_at(可空——本次唯一生产者"任务完成"不填这个字段,语义上是"立即到期";为未来 time 模式预留)。per-chat 支持多条待定记录(不是单槽位),同 pending_intent 的 per-scope 多记录先例。
  • 新增一个调度驱动(nonebot-plugin-apscheduler 定时任务,同 Drive Tick/反思闭环既有先例)扫描待送回的 Callback 记录:fire_at 为空或已到期即视为到期。到期记录按 chat_id 尝试送回;若该 chat 当前正被前台占用(复用 Drive Tick 已有的"忙时 defer"检查,同一个 _pending_flushes/等价追踪),顺延到下一次扫描,不强行插队造成并发写冲突。
  • 送回执行:到期的 Callback 内容作为普通内容重新进入一次带全套上下文的 Runtime Loop(复用 assemble_context() 五段拼装:安全基线→persona→冻结快照→触发注入→本次内容),无门控——不经过两级门控/沉默预算/自发目标的判定链,直接组装并请求模型基于这段内容组织语气回复。这不是一次新的输出出口,而是"履约"语义(同 ADR-0020 对 Callback 既有定性)。完成后清除对应 Callback 记录。
  • register_callback(用户主动设置到点提醒的工具)本次不实现——留给未来专门 ticket,届时只需新增一个工具把 fire_at 填成用户指定时间、复用本次已建好的存储/扫描/送回管线,不需要改动这次的地基。

成本归因(不做完整治理)

  • 深挖任务的 LLM 调用在日志/tracing 里标注所属"路径"为 delegate(概念上对应 ADR-0020 定义的独立成本池),但本次不实现任何预算额度/熔断//cost 可见性——完整六池成本治理(含另外五个现状同样零实现的池)是独立的运营与质量收尾工作,不在本次范围(见 Out of Scope)。

Testing Decisions

好的测试只测外部可观察行为(工具调用产生的效果、送回时机、并发/取消的最终状态),不测内部实现细节(如具体用哪种 asyncio 原语)。

  • 隔离任务上下文组装:无 IO 纯函数,直接断言给定任务描述产出的 context 列表结构(不含 persona/画像/历史)。同 drive_tick.py::assemble_light_context 的既有测试模式。
  • 子智能体工具循环:用 ScriptedLLMClient(仓库既有的脚本化假 LLM 测试双胞胎,见 test_reflection_cycle.py/test_persona_drift_guard.py 先例)驱动一串"调用工具→拿到结果→再调用→…→给出最终文本"的固定脚本,断言最终蒸馏文本、断言过程中不出现任何输出类工具调用、断言超过静态硬顶时强制收尾。
  • Callback 存储层:CRUD + per-chat 多条待定不互相覆盖,仿 test_pending_intent_storage.py 先例,跑在真实测试数据库上。
  • Callback 到期判定:纯函数,给定记录列表 + 当前时间,断言哪些算到期,仿 pending_intent.py::is_expired 的既有测试风格。
  • RuntimeLoop 前台工具处理delegate_task/cancel_delegate_task 两个新 handler 直接喂 RuntimeLoop.run() 脚本化对话断言效果(任务登记表状态、send_message 应声内容、重复请求时的提示、找不到任务时的 fail-closed 行为)——同现有 _handle_note_pending_intent 等工具处理器的测试模式。
  • 送回集成路径:断言到期 Callback 送回时确实调用了完整 assemble_context()(而不是走 run_light/事件驱动的裁剪版组装),且不经过 _evaluate_unified_gate
  • 取消竞态:构造"任务已完成、Callback 记录已生成、但尚未被下一次扫描送回"这个窗口期,断言此时调用 cancel_delegate_task 能让这条记录不再被送回。
  • 崩溃语义:复用现有崩溃恢复测试风格(recover()),断言进程重启后不会尝试"续跑"一个内存态任务登记表里已经不存在的任务(因为这类记录本就不持久化,不需要专门的清理逻辑,只需确认没有任何代码假设它能在重启后存活)。

Out of Scope

  • register_callback(用户主动设置到点提醒):Callback 的第二个触发源(time 模式),本次只建通用地基,不实现这个工具本身。
  • 完整成本治理/cost 可见性、六池预算+熔断、系统自检信号层):ADR-0010 完整运营面,独立于本次范围,Phase 4 PRD(issue #33)已经把它排除过一次,本次维持同样的边界。
  • 习得贴纸(ADR-0024)、跨平台资料拉取(ADR-0027)、记忆深度搜索(ADR-0031)、mood_congruence 接入三因子召回(ADR-0009 更新)、环境遥测 Ambient Telemetry(ADR-0014 更新):均为独立于深挖任务委托的其它未落地条目,各自体量足够单独立项,不在本次范围内顺带实现。
  • 子智能体递归/带危险工具/前台并行 fan-out:ADR-0020 已明确拒绝。
  • 模型自主选择自己用什么模型 / 按难度自动路由模型:ADR-0020 已明确拒绝/搁置。
  • 跑完再丢弃结果的"软取消":ADR-0020 已明确拒绝,取消一律真中断。
  • 基于话题漂移/超时的自动取消:ADR-0020 明确这是履约不是主动性决策,不适用红线3的"可抢占"。

Further Notes

  • ADR-0027(跨平台资料拉取,本次同样不在范围内)明确"复用本机制的执行形状但不复用治理"——它将来落地时会复用本次建的隔离上下文/子智能体工具循环的形状,但不会复用 Callback 的"履约不可抢占"语义(它没有"用户已请求"这个前提,仍需服红线3衰减/可抢占)。未来做那个 ticket 时需要重新确认这条边界,不要想当然把 Callback 送回语义整体套过去。
  • Callback 到期扫描的间隔、后台子智能体工具调用轮次硬顶、并发登记表的具体实现细节(是否需要给 task_id 加超时兜底防止 asyncio 任务本身悬挂)等具体数字/取舍,按 ADR-0011 既定原则留实现期按真实数据标定,不在本 PRD 里预先钉死。
  • ADR-0020 备注里提到的"登记表体现形式(内存字典 vs 落盘)"这个悬而未决的问题,本 PRD 已经拍板:内存字典(不落盘),理由见上 Implementation Decisions"任务登记"一节。
## Problem Statement 从终端用户角度:现在的 arise 只能在当轮 `run_limit`(默认 6 步推理工具调用)内直接给出回答。一旦用户提的问题需要"钻研"——比如"帮我分析总结一下我这千把条舞萌成绩打得怎么样"——这类任务原始数据量太大,不能直接倒进当轮 prompt(会撑爆上下文、破坏冻结快照的 prefix-cache),分析所需的步骤也太多、太多样,撑不满一个前台短循环。目前 arise 对这类请求要么勉强在 `run_limit` 内做浅层分析敷衍过去,要么干脆做不了。 从 host 部署者角度:CONTEXT.md/design.md 已经把"深挖任务委托"作为既定架构决策写进了文档(ADR-0020),但仓库里没有任何代码实现,也没有它依赖的"无门控送回"机制(Callback)。这个缺口挡在"能不能让 arise 真正处理复杂请求"和"现在只能处理三言两语能说清的问题"之间。 从维护者角度:ADR-0020 明确要求这类后台任务"异步执行、带工具、隔离任务上下文、不直接对用户说话",如果不从设计上把这几条约束落实为可测的边界,后续任何子智能体相关的改动都可能不小心开一条绕过人设/情感态调节的发言通道,或者让后台任务偷偷占用前台的成本/并发预算。 ## Solution 实现 [ADR-0020](../docs/adr/0020-deep-task-delegation.md) 定义的深挖任务委托机制:主模型识别到用户请求需要深挖时,调用新增核心工具 `delegate_task` 登记一个异步后台任务、紧跟一次 `send_message` 应一声("我看看啊")、当轮结束——不占用前台 `run_limit`/并发槽位。后台一个隔离上下文的子智能体(只带任务描述 + 工具港注册的 host 工具,无人设/记忆/关系/情感态/历史)自主完成多轮工具调用钻研数据,产出蒸馏文本结果。任务完成后,结果作为普通内容"无门控"重新进入一次带全套人设/记忆上下文的正常 Runtime Loop,由主模型自己组织语气、调 `send_message` 发出——子智能体全程不直接对用户说话。用户可用自然语言("算了不用查了")触发 `cancel_delegate_task`,真中断底层任务。 由于仓库里承接"任务完成后无门控重新进入会话"这个投递语义的 Callback 机制此前完全不存在(`pending_intent.py` 现有实现明确只覆盖 context 模式,time 模式/Callback 从未落地),本次顺带把 Callback 建成一个通用抽象(持久化、per-chat 多条待定、无门控直接送回),深挖任务完成是它的第一个真实触发源。`register_callback`(用户主动设置到点提醒)这个未来触发源本身不在本次范围内——只是让这次新建的存储/送回抽象足够通用,将来加它时只需要新增一个工具 + 一个到点扫描生产者,不需要改动本次建的地基。 ## User Stories **私聊/群聊用户视角:请求深挖** 1. 作为用户,我希望问机器人一个明显需要翻很多数据才能回答的问题(如"分析总结一下我这些成绩")时,它会先应一声"我看看啊"之类的话,而不是让我干等着不知道它有没有收到。 2. 作为用户,我希望机器人应声之后,我可以继续正常聊别的,不需要在原地等它,聊天不会被这个深挖任务卡住。 3. 作为用户,我希望深挖任务做完后,机器人是用它平时说话的语气把结论讲给我听,而不是甩给我一份格式化的分析报告或者原始数据转储。 4. 作为用户,我希望得到的结论是真的经过分析总结的,不是机器人不看数据就编出来的敷衍话。 5. 作为用户,我如果中途改主意了("算了不用查了"),我希望机器人能听懂并真的停下这个后台任务,而不是嘴上说"好的"但后台还在跑、过一会儿突然又把结果发给我。 6. 作为用户,我希望同一个对话里一次只有一个深挖任务在跑——如果我在前一个还没做完时又提了一个新的深挖请求,机器人会告诉我"之前那个还在弄",而不是同时开好几个把系统拖垮。 7. 作为私聊用户,我希望深挖任务的结果送回到我们这个私聊,不会送错地方。 8. 作为群聊用户,我希望深挖任务是我在这个群里请求的,结果也送回这个群,即使这段时间群里其它人也在跟机器人聊别的。 9. 作为用户,我希望深挖任务做完、结果送回来的这次发言,跟机器人平时主动找我说话(Recall/自发目标)不是一回事——这次是我请求过的,机器人不会因为"沉默预算"不够或"现在不是合适时机"就把结果压下不发。 **Host 部署者/运维者视角:安全与可控** 10. 作为部署者,我希望后台子智能体只能调用我通过工具港注册给它的那些工具,不会被额外授予任何我没注册过的能力。 11. 作为部署者,我希望后台子智能体不能直接绕过我配置的 persona/情感态调节直接给用户发消息——所有对外发言都经过正常的、带人设的主循环。 12. 作为部署者,我希望深挖任务的后台工具调用轮次有一个静态硬顶,不会因为子智能体陷入死循环或数据异常复杂而无限跑下去、无限消耗 token。 13. 作为部署者,我希望进程重启时,正在跑的深挖任务直接丢失(不重放),这跟 arise 现有的崩溃恢复哲学(丢弃在途请求)一致,不会留下奇怪的残留状态或重复送达。 14. 作为部署者,我希望不同 chat 的深挖任务互不影响并发上限——A 群有一个任务在跑不会挡住 B 群发起自己的深挖任务。 15. 作为部署者,我希望这次新建的送回机制(Callback)以后能直接拿来支撑"用户设置到点提醒"这类需求,不用推倒重来。 **维护者/贡献者视角:可测性** 16. 作为维护者,我希望"给子智能体组装隔离任务上下文(任务描述+工具,不含人设/记忆/历史)"是一个不需要真实 LLM 调用就能直接断言其内容的纯函数。 17. 作为维护者,我希望子智能体的多轮工具调用循环可以用脚本化的假 LLM 客户端(复用现有 `ScriptedLLMClient` 先例)驱动,断言"给定一串工具调用/返回,最终产出预期的蒸馏文本",不需要真实网络请求。 18. 作为维护者,我希望 `delegate_task`/`cancel_delegate_task` 这两个新工具在 `RuntimeLoop` 里的处理逻辑,跟现有 `note_pending_intent` 等工具处理器一样,可以直接喂 `RuntimeLoop.run()` 一段脚本化对话来测试,不需要真的启动后台任务等待完成。 19. 作为维护者,我希望 Callback 存储层(新增/查询/删除待定记录、per-chat 多条不互相覆盖)有独立于调度驱动的存储集成测试,同 `pending_intent` 现有的纯函数测试 + 存储集成测试两层先例。 20. 作为维护者,我希望"哪些待定 Callback 现在算到期该送回"这个判定是一个不依赖真实时钟/调度器的纯函数,可以直接构造固定输入断言结果。 21. 作为维护者,我希望子智能体工具调用失败时,这次失败被当作一次普通的失败 tool_response 处理(同工具港既有的"不自动重试"哲学),不会让整个后台任务未经处理地崩溃、也不会让调用方看不到任何反馈就静默卡死。 **验收标准视角** 22. 作为验收标准,我希望后台深挖任务的工具调用轮次不计入触发它的那一轮前台 `run_limit`。 23. 作为验收标准,我希望子智能体被赋予的工具集里不包含 `send_message`/`self_recall`/`edit` 等任何输出类工具——结构上就不可能绕开主循环直接发言。 24. 作为验收标准,我希望蒸馏结果送回时经过的是完整的 `assemble_context()`(安全基线 + persona + 冻结记忆快照 + 触发注入),不是一个阉割版上下文。 25. 作为验收标准,我希望 `cancel_delegate_task` 在任务已经完成、结果还没来得及送回这个窄时间窗口内被调用时,也能确实压下这次送回,不会出现"说了算了、结果还是发过来了"的情况。 26. 作为验收标准,我希望 Callback 的送回不占用/不消耗两级门控的沉默预算——它是履约通道,不是主动性决策通道。 27. 作为验收标准,我希望新建的 Callback 存储结构支持一个 chat 同时存在多条待定记录,不会因为深挖任务完成通知占用了唯一槽位而挤掉其它待定项(即便本次没有其它生产者,也不应该是单槽位设计)。 ## Implementation Decisions **新增核心工具(RuntimeLoop 层,前台)** - `delegate_task(description)`:core 原生工具(同 `note_pending_intent`/`search_memory` 先例,非工具港 host 工具),当轮调用即在进程内登记一个后台任务(chat_id/task_id → 运行中任务句柄),随即以 `asyncio` 方式启动后台执行,不阻塞当轮工具循环,也不计入本轮 `run_limit`/`MAX_SENDS_PER_TURN`(本身不是 send)。模型通常紧跟一次 `send_message` 应声,当轮随后自然结束。 - `cancel_delegate_task(task_id?)`:core 原生工具,模型在对话中识别到用户要求取消时调用。若对应任务仍在跑,真中断底层 `asyncio` 任务;若任务已完成但结果尚未送回(见下 Callback 窗口),一并撤销这条待送回记录。找不到对应任务/已经送达过的任务,视为无操作(fail-closed,不报错阻断对话)。 - 每个 chat 同时至多一个深挖任务在跑(进程内按 chat_id 检查);重复请求时 `delegate_task` 直接返回一个可读的"已有任务在跑"结果给模型,由模型组织语言告知用户,不新起第二个任务。 - 这两个工具的可见性不受渐进解锁门控(不同于自发目标)——深挖任务是用户显式请求触发,不是机器人自主决定的主动性功能,不适用渐进解锁"新部署更保守"的理由。 **任务登记(进程内存态,不持久化)** - chat_id → 运行中任务句柄(`asyncio.Task` + task_id)的登记表是进程内存字典,同现有 `_pending_flushes`/`_pending_followup_checks` 先例,不落盘。进程崩溃/重启即丢失在途任务,不重放——与 ADR-0013 崩溃恢复语义一致,不是本次新开的例外。 **后台子智能体执行(新模块,隔离上下文)** - 新增一个独立模块(同 `reflection.py`/`persona_drift_guard.py` 的既有分层先例),职责是:① 组装隔离任务上下文(纯函数:只含任务描述 + 安全基线,不含 persona/画像/关系网/情感态/历史对话);② 驱动一个独立的多轮工具调用循环,使用 `ports.llm_client`(主力模型——当前仓库还没有独立配置的 auxiliary 模型客户端,`reflection.py` 现状也是复用同一个 `llm_client`,故这里天然满足 ADR-0020"用主力模型"的要求,不需要新增模型路由);③ 循环终止条件是子智能体自己表示"分析完毕"(不再调用工具,给出最终文本),产出该文本作为蒸馏结果。 - 子智能体可见工具集 = 工具港 `get_tools("background")` 返回的 host 工具(`RuntimeContext.background` 这个既有契约取值首次被真正接入调用点)。子智能体工具集里不包含 core 自己的任何输出类工具(`send_message`/`self_recall`/`edit`/`delegate_task` 本身等)——结构性排除,不是运行时判断。 - 后台工具调用轮次设独立静态硬顶(新增 config 项,不复用前台 `arise_run_limit`——量级不同,具体数字按 ADR-0011 既定原则推迟到真实数据标定),超过硬顶即强制结束循环,把已经拿到的内容作为蒸馏结果(不是直接判定失败——尽量拿现有信息交差,同 ADR-0028"不阻塞回复"的一贯风格)。 - 子智能体工具调用失败按工具港既有哲学处理(失败即一次普通 tool_response,不自动重试),不中断整个后台任务。 **Callback(通用送回抽象,新建)** - 新增存储 port 切片:持久化的 Callback 记录,字段含 id、chat_id、content(送回时要注入的内容)、trigger_source(本次只有 `"task_completion"` 一个取值,字段本身开放给未来扩展如 `"user_reminder"`)、created_at、fire_at(可空——本次唯一生产者"任务完成"不填这个字段,语义上是"立即到期";为未来 time 模式预留)。per-chat 支持多条待定记录(不是单槽位),同 `pending_intent` 的 per-scope 多记录先例。 - 新增一个调度驱动(`nonebot-plugin-apscheduler` 定时任务,同 Drive Tick/反思闭环既有先例)扫描待送回的 Callback 记录:`fire_at` 为空或已到期即视为到期。到期记录按 chat_id 尝试送回;若该 chat 当前正被前台占用(复用 Drive Tick 已有的"忙时 defer"检查,同一个 `_pending_flushes`/等价追踪),顺延到下一次扫描,不强行插队造成并发写冲突。 - 送回执行:到期的 Callback 内容作为普通内容重新进入一次带全套上下文的 Runtime Loop(复用 `assemble_context()` 五段拼装:安全基线→persona→冻结快照→触发注入→本次内容),**无门控**——不经过两级门控/沉默预算/自发目标的判定链,直接组装并请求模型基于这段内容组织语气回复。这不是一次新的输出出口,而是"履约"语义(同 ADR-0020 对 Callback 既有定性)。完成后清除对应 Callback 记录。 - `register_callback`(用户主动设置到点提醒的工具)本次不实现——留给未来专门 ticket,届时只需新增一个工具把 `fire_at` 填成用户指定时间、复用本次已建好的存储/扫描/送回管线,不需要改动这次的地基。 **成本归因(不做完整治理)** - 深挖任务的 LLM 调用在日志/tracing 里标注所属"路径"为 `delegate`(概念上对应 ADR-0020 定义的独立成本池),但本次不实现任何预算额度/熔断/`/cost` 可见性——完整六池成本治理(含另外五个现状同样零实现的池)是独立的运营与质量收尾工作,不在本次范围(见 Out of Scope)。 ## Testing Decisions 好的测试只测外部可观察行为(工具调用产生的效果、送回时机、并发/取消的最终状态),不测内部实现细节(如具体用哪种 asyncio 原语)。 - **隔离任务上下文组装**:无 IO 纯函数,直接断言给定任务描述产出的 context 列表结构(不含 persona/画像/历史)。同 `drive_tick.py::assemble_light_context` 的既有测试模式。 - **子智能体工具循环**:用 `ScriptedLLMClient`(仓库既有的脚本化假 LLM 测试双胞胎,见 `test_reflection_cycle.py`/`test_persona_drift_guard.py` 先例)驱动一串"调用工具→拿到结果→再调用→…→给出最终文本"的固定脚本,断言最终蒸馏文本、断言过程中不出现任何输出类工具调用、断言超过静态硬顶时强制收尾。 - **Callback 存储层**:CRUD + per-chat 多条待定不互相覆盖,仿 `test_pending_intent_storage.py` 先例,跑在真实测试数据库上。 - **Callback 到期判定**:纯函数,给定记录列表 + 当前时间,断言哪些算到期,仿 `pending_intent.py::is_expired` 的既有测试风格。 - **RuntimeLoop 前台工具处理**:`delegate_task`/`cancel_delegate_task` 两个新 handler 直接喂 `RuntimeLoop.run()` 脚本化对话断言效果(任务登记表状态、`send_message` 应声内容、重复请求时的提示、找不到任务时的 fail-closed 行为)——同现有 `_handle_note_pending_intent` 等工具处理器的测试模式。 - **送回集成路径**:断言到期 Callback 送回时确实调用了完整 `assemble_context()`(而不是走 `run_light`/事件驱动的裁剪版组装),且不经过 `_evaluate_unified_gate`。 - **取消竞态**:构造"任务已完成、Callback 记录已生成、但尚未被下一次扫描送回"这个窗口期,断言此时调用 `cancel_delegate_task` 能让这条记录不再被送回。 - **崩溃语义**:复用现有崩溃恢复测试风格(`recover()`),断言进程重启后不会尝试"续跑"一个内存态任务登记表里已经不存在的任务(因为这类记录本就不持久化,不需要专门的清理逻辑,只需确认没有任何代码假设它能在重启后存活)。 ## Out of Scope - **`register_callback`(用户主动设置到点提醒)**:Callback 的第二个触发源(time 模式),本次只建通用地基,不实现这个工具本身。 - **完整成本治理**(`/cost` 可见性、六池预算+熔断、系统自检信号层):ADR-0010 完整运营面,独立于本次范围,Phase 4 PRD(issue #33)已经把它排除过一次,本次维持同样的边界。 - **习得贴纸(ADR-0024)、跨平台资料拉取(ADR-0027)、记忆深度搜索(ADR-0031)、mood_congruence 接入三因子召回(ADR-0009 更新)、环境遥测 Ambient Telemetry(ADR-0014 更新)**:均为独立于深挖任务委托的其它未落地条目,各自体量足够单独立项,不在本次范围内顺带实现。 - **子智能体递归/带危险工具/前台并行 fan-out**:ADR-0020 已明确拒绝。 - **模型自主选择自己用什么模型 / 按难度自动路由模型**:ADR-0020 已明确拒绝/搁置。 - **跑完再丢弃结果的"软取消"**:ADR-0020 已明确拒绝,取消一律真中断。 - **基于话题漂移/超时的自动取消**:ADR-0020 明确这是履约不是主动性决策,不适用红线3的"可抢占"。 ## Further Notes - [ADR-0027](../docs/adr/0027-cross-platform-profile-pull.md)(跨平台资料拉取,本次同样不在范围内)明确"复用本机制的执行形状但不复用治理"——它将来落地时会复用本次建的隔离上下文/子智能体工具循环的形状,但**不会**复用 Callback 的"履约不可抢占"语义(它没有"用户已请求"这个前提,仍需服红线3衰减/可抢占)。未来做那个 ticket 时需要重新确认这条边界,不要想当然把 Callback 送回语义整体套过去。 - Callback 到期扫描的间隔、后台子智能体工具调用轮次硬顶、并发登记表的具体实现细节(是否需要给 task_id 加超时兜底防止 asyncio 任务本身悬挂)等具体数字/取舍,按 ADR-0011 既定原则留实现期按真实数据标定,不在本 PRD 里预先钉死。 - ADR-0020 备注里提到的"登记表体现形式(内存字典 vs 落盘)"这个悬而未决的问题,本 PRD 已经拍板:内存字典(不落盘),理由见上 Implementation Decisions"任务登记"一节。
Yushu closed this issue 2026-07-24 08:40:29 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ProjectKuma/arise#52
No description provided.