6.6 KiB
6.6 KiB
| name | description |
|---|---|
| background_notification_pool | 当需要讨论或修复后台子智能体/后台 run_command 完成通知机制时,应该索引本记忆 |
后台子智能体 / 后台 run_command 通知机制问题(待修复)
问题描述
后台子智能体和后台 run_command 在“主智能体停止输出后完成”的场景下,通知机制存在串行化问题:
- 若同时有多个后台任务在运行,它们不会随完成逐个通知。
- 而是在所有后台任务全部完成后,按时间顺序一条一条地发送 user 消息。
- 且每发送一条 user 消息就要触发一次“工作 → 停止 → 再工作”的循环,导致通知被拆成多次停止输出。
当前机制
运行期间(inline)和停止输出后(polling)是两条链路:
1. 运行期间(inline,当前正常)
- 位置:
server/chat_flow_tool_loop.py中execute_tool_calls末尾。 - 调用:
process_sub_agent_updates(..., inline=True)process_background_command_updates(..., inline=True)
- 行为:每次工具循环结束时,把当前所有已完成且未通知的任务一次性插入
messages,不触发新任务。 - 结论:运行期间的批量通知已经是“池化”的,暂无问题。
2. 停止输出后(polling,问题所在)
- 位置:
server/chat_flow_task_main.py中handle_task_with_sender结尾。 - 调用:
poll_sub_agent_completion(...)poll_background_command_completion(...)
- 行为:
- 两个轮询器各自由
socketio.start_background_task(...)启动。 - 每轮循环只处理一个完成的通知。
- 处理完一个通知后,创建一个新的 chat task,然后
return。 - 下一个通知必须等待这个新 chat task 跑完,并再次走到
handle_task_with_sender结尾重新 spawn 轮询器时才会发出。
- 两个轮询器各自由
根因
后台任务管理器本身已经扮演了“后台池 + 通知池”的角色(任务完成但 notified=False 就是待通知状态)。
但轮询器是 one-shot 的:每次只消费一条通知就退出,导致通知被串行化到多次“工作 → 停止 → 再工作”循环中。
讨论中的设计方案
统一为“通知池轮询器”:
- 合并
poll_sub_agent_completion和poll_background_command_completion为一个统一的poll_completion_notifications。 - 每次轮询时,同时从以下两处取出所有待通知项:
sub_agent_manager.poll_updates()background_command_manager.poll_updates(conversation_id=...)
- 把所有待通知项作为 user 消息插入对话历史(倾向保留为多条消息,而非合并成一条)。
- 只创建 一个后续 chat task 来触发继续工作。
- 轮询器返回,由这个新任务在结束时继续下一轮轮询。
期望行为示例
- 3 个后台子智能体运行中,主智能体停止输出。
- 第 1 个完成 → 通知池有 1 条 → 插入 1 条 user 消息 → 触发 1 个新任务。
- 新任务处理期间,第 2、3 个完成 → 进入通知池。
- 新任务停止输出 → 轮询器发现通知池还有 2 条 → 一次性插入 2 条 user 消息 → 再触发 1 个新任务。
- 不再有后台任务时,轮询器直接退出。
涉及文件
| 文件 | 角色 |
|---|---|
server/chat_flow_task_support.py |
新增统一轮询器 poll_completion_notifications;可废弃旧的两个轮询函数。 |
server/chat_flow_task_main.py |
在 handle_task_with_sender 结尾,把分别 spawn 两个轮询器的逻辑替换为 spawn 一个统一轮询器。 |
modules/sub_agent/manager.py |
提供 poll_updates(),基于 notified 标志表示待通知状态。 |
modules/background_command_manager.py |
提供 poll_updates() 和 has_pending_for_conversation()。 |
待决策点
- 是否坚持把通知拆成多条 user 消息插入历史(推荐),还是合并为一条。
- 统一轮询器是否做成“持久单例”还是保持 one-shot batch(当前讨论倾向 one-shot batch,由新任务继续轮询)。
状态
已实施(2026-06-24,方案 A + one-shot batch)。
落地实现
server/chat_flow_task_support.py未改;新增逻辑集中在server/chat_flow_task_main.py。- 新增统一轮询器
poll_completion_notifications,替换并删除旧的poll_sub_agent_completion/poll_background_command_completion。 - 新增
_collect_pending_completion_notices:每轮一次性从子智能体 + 后台命令两路取出所有待通知项(取出即就地标记notified/_announced_sub_agent_tasks),按updated_at跨两路排序。 - 新增
_has_pending_completion_work:判断是否还有运行中/待通知的后台工作,决定轮询是否继续。 - 新增
_build_sub_agent_notice_text:抽出子智能体通知文本构建。 handle_task_with_sender结尾:保留sub_agent_waiting事件探测,但把「分别 spawn 两个轮询器」改为只 spawn 一个run_completion_poll。这也顺带消除了两个轮询器同时create_chat_task撞create_chat_task单工作区互斥(models.pyRuntimeError)的隐患。
方案 A 的「多条 + 单 task」如何落地
create_chat_task 只收单条 message,handle_task_with_sender 开头无条件 add_conversation。因此:
- 一批取出 N 条:前 N-1 条为「前置通知」,最后 1 条作为后续 chat task 的触发消息。
_dispatch_completion_user_notice新增preceding_notices参数:- 通过
_persist_and_echo_preceding_notice把前 N-1 条写入对话历史(add_conversation,落盘)+ socketio 回显(在线客户端即时可见),starts_work=False(渲染为完成态气泡,不起独立计时器)。 - 把
preceding_user_notices塞进session_data,传给后续任务。
- 通过
server/tasks/models.py的_run_chat_task:在补发触发 user_message 事件之前,先按顺序把preceding_user_notices逐条_append_event("user_message", ...)注入任务事件流——保证 REST 轮询客户端按时间顺序看到所有完成通知。- 触发消息(最后 1 条)走原有
auto_user_message_event链路,正常starts_work=True启动一轮工作。
验证
python3 -m py_compile server/chat_flow_task_main.py server/tasks/models.py通过(注意本机 pycache 目录无写权限,需PYTHONPYCACHEPREFIX=$TMPDIR/pycache)。python3 -m unittest test.test_server_refactor_smoke6 用例通过。- 前端未改:复用既有
user_message/sub_agent_notice渲染链路(messaging.tshandleUserMessage + socketiouser_message),dedup 与单通知场景一致。