agent-Specialization/.astrion/memory/background_notification_pool.md

6.6 KiB
Raw Blame History

name description
background_notification_pool 当需要讨论或修复后台子智能体/后台 run_command 完成通知机制时,应该索引本记忆

后台子智能体 / 后台 run_command 通知机制问题(待修复)

问题描述

后台子智能体和后台 run_command 在“主智能体停止输出后完成”的场景下,通知机制存在串行化问题:

  • 若同时有多个后台任务在运行,它们不会随完成逐个通知。
  • 而是在所有后台任务全部完成后,按时间顺序一条一条地发送 user 消息。
  • 且每发送一条 user 消息就要触发一次“工作 → 停止 → 再工作”的循环,导致通知被拆成多次停止输出。

当前机制

运行期间inline和停止输出后polling是两条链路

1. 运行期间inline当前正常

  • 位置:server/chat_flow_tool_loop.pyexecute_tool_calls 末尾。
  • 调用:
    • process_sub_agent_updates(..., inline=True)
    • process_background_command_updates(..., inline=True)
  • 行为:每次工具循环结束时,把当前所有已完成且未通知的任务一次性插入 messages,不触发新任务。
  • 结论:运行期间的批量通知已经是“池化”的,暂无问题。

2. 停止输出后polling问题所在

  • 位置:server/chat_flow_task_main.pyhandle_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_completionpoll_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_taskcreate_chat_task 单工作区互斥(models.py RuntimeError的隐患。

方案 A 的「多条 + 单 task」如何落地

create_chat_task 只收单条 messagehandle_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_messagededup 与单通知场景一致。