110 lines
6.6 KiB
Markdown
110 lines
6.6 KiB
Markdown
---
|
||
name: background_notification_pool
|
||
description: 当需要讨论或修复后台子智能体/后台 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** 的:每次只消费一条通知就退出,导致通知被串行化到多次“工作 → 停止 → 再工作”循环中。
|
||
|
||
## 讨论中的设计方案
|
||
|
||
统一为“通知池轮询器”:
|
||
|
||
1. 合并 `poll_sub_agent_completion` 和 `poll_background_command_completion` 为一个统一的 `poll_completion_notifications`。
|
||
2. 每次轮询时,同时从以下两处取出**所有**待通知项:
|
||
- `sub_agent_manager.poll_updates()`
|
||
- `background_command_manager.poll_updates(conversation_id=...)`
|
||
3. 把所有待通知项作为 user 消息插入对话历史(倾向保留为多条消息,而非合并成一条)。
|
||
4. 只创建 **一个**后续 chat task 来触发继续工作。
|
||
5. 轮询器返回,由这个新任务在结束时继续下一轮轮询。
|
||
|
||
### 期望行为示例
|
||
|
||
- 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.py` RuntimeError)的隐患。**
|
||
|
||
### 方案 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_smoke` 6 用例通过。
|
||
- 前端未改:复用既有 `user_message` / `sub_agent_notice` 渲染链路(`messaging.ts` handleUserMessage + socketio `user_message`),dedup 与单通知场景一致。
|