fix(multi-agent): 修复情况2主消息池派发链路

修复多智能体模式下主智能体空闲时收不到子智能体输出推送的一系列 bug:

1. poll_multi_agent_notifications 死锁:原实现先等所有 running 实例
   退出再 drain 消息池,导致 ask_master 在 await 期间永远等不到主对话
   回答。改为池优先:pool 有消息立即派发,不管 running 状态。

2. _dispatch_multi_agent_idle_messages 缺 import:调用
   inject_multi_agent_master_message 但文件顶部从未导入,NameError
   被外层 except 吞掉,task 永远建不起来。

3. dispatch 内调试日志引用 rec 错位:dispatch_ma_idle_sender_user_message
   被放到 create_chat_task 之前,触发 UnboundLocalError,task 同样建不起来。

4. session_data['auto_user_message_payload'] / preceding_user_notices
   payload 漏写 auto_message_type:前端 isMultiAgentMessage() 只认
   auto_message_type.startsWith('multi_agent_'),空字符串走 fallback
   通知渲染。

5. dispatch 第①步重复持久化:对包含 last 的全部消息都调
   inject_multi_agent_master_message 落盘,之后 task 又在 handle_task_with_sender
   再 add_conversation,导致历史里出现两条相同 user 消息(前一条多智能体渲染,
   后一条通知渲染)。前置 N-1 条只持久化一次,最后一条交给后续 task 自己持久化。

6. last 赋值时机错位:last_emit_payload 在 last=parsed_messages[-1] 之前引用,
   UnboundLocalError 再次吃掉后续链路。

7. handle_task_with_sender 多智能体分支漏写 visibility='chat':
   _user_message_ui_defaults('sub_agent') 默认 visibility='compact',
   透传到落盘 metadata 后,前端从后端加载历史时走通知渲染分支。显式
   user_message_metadata['visibility']='chat' 强制走多智能体专用渲染。
This commit is contained in:
JOJO 2026-07-13 20:05:02 +08:00
parent fe6fba3958
commit e29ccb318e
15 changed files with 911 additions and 91 deletions

View File

@ -386,6 +386,14 @@ class MessagesMixin:
# 调试:记录所有 system 消息 # 调试:记录所有 system 消息
if conv["role"] == "system": if conv["role"] == "system":
logger.info(f"[DEBUG build_messages] 添加 system 消息: content前50字={conv['content'][:50]}") logger.info(f"[DEBUG build_messages] 添加 system 消息: content前50字={conv['content'][:50]}")
# 调试:记录多智能体子智能体输出消息是否进入上下文
if metadata.get("multi_agent_message"):
logger.info(
f"[DEBUG build_messages] 添加多智能体子智能体消息到上下文: "
f"role={conv['role']}, subtype={metadata.get('multi_agent_subtype')}, "
f"display_name={metadata.get('multi_agent_display_name')}, "
f"content前80字={str(conv.get('content', ''))[:80]}"
)
messages.append({ messages.append({
"role": conv["role"], "role": conv["role"],
"content": content_payload "content": content_payload

View File

@ -328,12 +328,26 @@ class MultiAgentState:
# ----- 主对话注入 ----- # ----- 主对话注入 -----
def push_master_message(self, message_text: str) -> None: def push_master_message(self, message_text: str) -> None:
"""把一条 user 消息追加到主对话待插入队列。""" """把一条 user 消息追加到主对话待插入队列。"""
ma_debug(
"state_push_master_message",
conversation_id=self.conversation_id,
state_id=id(self),
queue_len_before=len(self.pending_master_messages),
msg_preview=str(message_text)[:200],
)
self.pending_master_messages.append(message_text) self.pending_master_messages.append(message_text)
def drain_master_messages(self) -> List[str]: def drain_master_messages(self) -> List[str]:
"""取出(清空)所有待插入主对话的消息。""" """取出(清空)所有待插入主对话的消息。"""
msgs = self.pending_master_messages msgs = self.pending_master_messages
self.pending_master_messages = [] self.pending_master_messages = []
ma_debug(
"state_drain_master_messages",
conversation_id=self.conversation_id,
state_id=id(self),
drained_count=len(msgs),
previews=[str(m)[:150] for m in msgs],
)
return msgs return msgs
def has_pending_master_messages(self) -> bool: def has_pending_master_messages(self) -> bool:

View File

@ -275,6 +275,13 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
multi_agent_state=multi_agent_state, multi_agent_state=multi_agent_state,
display_name=display_name, display_name=display_name,
) )
ma_debug(
"manager_create_sub_agent_state",
task_id=task_id,
agent_id=agent_id,
conversation_id=conversation_id,
state_id=id(multi_agent_state) if multi_agent_state else None,
)
task_coro = sub_agent.run() task_coro = sub_agent.run()
asyncio_task = self._run_coro(task_coro) asyncio_task = self._run_coro(task_coro)
sub_agent._task = asyncio_task sub_agent._task = asyncio_task
@ -283,12 +290,16 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
self._sub_agent_instances[agent_id] = sub_agent self._sub_agent_instances[agent_id] = sub_agent
def _on_done(fut): def _on_done(fut):
try:
self._running_tasks.pop(task_id, None) self._running_tasks.pop(task_id, None)
self._sub_agent_instances.pop(agent_id, None) self._sub_agent_instances.pop(agent_id, None)
self.reconcile_task_states(conversation_id=conversation_id) self.reconcile_task_states(conversation_id=conversation_id)
# 多智能体模式:结束时把状态写回 MultiAgentState # 多智能体模式:结束时把状态写回 MultiAgentState
if multi_agent_mode and multi_agent_state: if multi_agent_mode and multi_agent_state:
self._on_multi_agent_task_done(task_id, agent_id, multi_agent_state, sub_agent) self._on_multi_agent_task_done(task_id, agent_id, multi_agent_state, sub_agent)
except Exception as exc:
logger.exception(f"[SubAgent] task {task_id} 完成回调异常: {exc}")
ma_debug("manager_on_done_exception", task_id=task_id, agent_id=agent_id, error=str(exc))
asyncio_task.add_done_callback(_on_done) asyncio_task.add_done_callback(_on_done)
@ -704,11 +715,15 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
self._sub_agent_instances[agent_id] = sub_agent self._sub_agent_instances[agent_id] = sub_agent
def _on_done(fut, tid=task_id, aid=agent_id, state=multi_agent_state, sa=sub_agent): def _on_done(fut, tid=task_id, aid=agent_id, state=multi_agent_state, sa=sub_agent):
try:
self._running_tasks.pop(tid, None) self._running_tasks.pop(tid, None)
self._sub_agent_instances.pop(aid, None) self._sub_agent_instances.pop(aid, None)
self.reconcile_task_states(conversation_id=conversation_id) self.reconcile_task_states(conversation_id=conversation_id)
if multi_agent_mode and state: if multi_agent_mode and state:
self._on_multi_agent_task_done(tid, aid, state, sa) self._on_multi_agent_task_done(tid, aid, state, sa)
except Exception as exc:
logger.exception(f"[SubAgent] restored task {tid} 完成回调异常: {exc}")
ma_debug("manager_restore_on_done_exception", task_id=tid, agent_id=aid, error=str(exc))
asyncio_task.add_done_callback(_on_done) asyncio_task.add_done_callback(_on_done)
restored += 1 restored += 1
@ -734,14 +749,34 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
from modules.multi_agent.state import MultiAgentState from modules.multi_agent.state import MultiAgentState
state = self.multi_agent_states.get(conversation_id) state = self.multi_agent_states.get(conversation_id)
if state: if state:
ma_debug(
"manager_get_or_create_ma_state_reuse",
conversation_id=conversation_id,
state_id=id(state),
manager_id=id(self),
)
return state return state
state = MultiAgentState(conversation_id=conversation_id) state = MultiAgentState(conversation_id=conversation_id)
self.multi_agent_states[conversation_id] = state self.multi_agent_states[conversation_id] = state
ma_debug(
"manager_get_or_create_ma_state_create",
conversation_id=conversation_id,
state_id=id(state),
manager_id=id(self),
)
return state return state
def get_multi_agent_state(self, conversation_id: str): def get_multi_agent_state(self, conversation_id: str):
"""获取该会话的多智能体状态。""" """获取该会话的多智能体状态。"""
return self.multi_agent_states.get(conversation_id) state = self.multi_agent_states.get(conversation_id)
ma_debug(
"manager_get_multi_agent_state",
conversation_id=conversation_id,
found=bool(state),
state_id=id(state) if state else None,
manager_id=id(self),
)
return state
def drop_multi_agent_state(self, conversation_id: str) -> None: def drop_multi_agent_state(self, conversation_id: str) -> None:
"""删除会话状态(会话结束时调用)。""" """删除会话状态(会话结束时调用)。"""
@ -765,7 +800,47 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
): ):
task["multi_agent_mode"] = True task["multi_agent_mode"] = True
task["updated_at"] = time.time() task["updated_at"] = time.time()
return super().reconcile_task_states(conversation_id=conversation_id) changed = super().reconcile_task_states(conversation_id=conversation_id)
# 多智能体模式下,子智能体进入 idle 后底层 asyncio.Task 仍在等待唤醒,
# 父类 reconcile 会据此把任务标回 running。这里根据内存中的 SubAgentTask
# 实例重新把 idle 状态写回任务记录,使运行态与 MultiAgentState 保持一致。
extra_changed = 0
for task in self.tasks.values():
if not isinstance(task, dict):
continue
if conversation_id and task.get("conversation_id") != conversation_id:
continue
if not task.get("multi_agent_mode"):
continue
agent_id = task.get("agent_id")
inst = self._sub_agent_instances.get(agent_id) if agent_id else None
if inst:
if getattr(inst, "_idle", False):
if task.get("status") != "idle":
task["status"] = "idle"
task["updated_at"] = time.time()
ma_debug(
"reconcile_task_runtime_state_idle_fix",
task_id=task.get("task_id"),
agent_id=agent_id,
)
extra_changed += 1
elif task.get("status") == "idle":
# 子智能体已被唤醒且 _idle=false但 output 文件或父类 reconcile
# 可能仍把任务标为 idle。这里强制同步回 running。
task["status"] = "running"
task["updated_at"] = time.time()
ma_debug(
"reconcile_task_runtime_state_running_fix",
task_id=task.get("task_id"),
agent_id=agent_id,
)
extra_changed += 1
if extra_changed:
self._save_state()
changed += extra_changed
return changed
def inject_message_to_sub_agent(self, agent_id: int, message_text: str) -> bool: def inject_message_to_sub_agent(self, agent_id: int, message_text: str) -> bool:
"""同事件循环中向子智能体上下文插入 user 消息。 """同事件循环中向子智能体上下文插入 user 消息。
@ -828,6 +903,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
return return
# 兜底:取出当前 task status由 _finalize_task 设置) # 兜底:取出当前 task status由 _finalize_task 设置)
final_status = final_task.get("status")
if final_status in TERMINAL_STATUSES: if final_status in TERMINAL_STATUSES:
state.mark_status(agent_id, final_status, last_output=str(final_task.get("final_result") or "")) state.mark_status(agent_id, final_status, last_output=str(final_task.get("final_result") or ""))
ma_debug("manager_ma_state_set", agent_id=agent_id, status=final_status, reason="task_terminal_status") ma_debug("manager_ma_state_set", agent_id=agent_id, status=final_status, reason="task_terminal_status")

View File

@ -62,7 +62,22 @@ class SubAgentStateMixin:
for conv_id, snapshot in loaded_ma_states.items(): for conv_id, snapshot in loaded_ma_states.items():
try: try:
if isinstance(snapshot, dict): if isinstance(snapshot, dict):
# 关键:不要覆盖内存中已存在的 MultiAgentState
# 否则 SubAgentTask 持有的旧引用上的 pending_master_messages
# 会被新的空 state 覆盖,导致子智能体输出丢失。
if conv_id in multi_agent_states:
ma_debug(
"load_state_skip_existing_ma_state",
conversation_id=conv_id,
existing_state_id=id(multi_agent_states[conv_id]),
)
continue
multi_agent_states[conv_id] = MultiAgentState.from_snapshot(snapshot) multi_agent_states[conv_id] = MultiAgentState.from_snapshot(snapshot)
ma_debug(
"load_state_restore_ma_state",
conversation_id=conv_id,
state_id=id(multi_agent_states[conv_id]),
)
except Exception as exc: except Exception as exc:
logger.warning(f"恢复多智能体状态失败 {conv_id}: {exc}") logger.warning(f"恢复多智能体状态失败 {conv_id}: {exc}")
except Exception as exc: except Exception as exc:

View File

@ -180,6 +180,16 @@ class SubAgentTask:
pending_messages=[m.get("role") for m in self.messages[-3:]], pending_messages=[m.get("role") for m in self.messages[-3:]],
) )
self._idle = False self._idle = False
# 关键修复:从 idle 唤醒后必须同步更新 MultiAgentState 状态为 running
# 否则 has_running_multi_agent 会误判为 false导致主对话提前进入空闲、
# 后续子智能体输出无法推送到主智能体。
if self.multi_agent_state:
self.multi_agent_state.mark_status(self.agent_id, "running")
ma_debug(
"sub_agent_idle_wake_mark_running",
task_id=self.task_id,
agent_id=self.agent_id,
)
continue continue
turn += 1 turn += 1
@ -272,7 +282,19 @@ class SubAgentTask:
def _forward_output_to_master(self, output_text: str, *, is_final: bool = False) -> None: def _forward_output_to_master(self, output_text: str, *, is_final: bool = False) -> None:
"""把子智能体的 assistant 文本输出转发成主对话的 user 消息,并写入进度文件供前端查看。""" """把子智能体的 assistant 文本输出转发成主对话的 user 消息,并写入进度文件供前端查看。"""
ma_debug(
"sub_agent_forward_output_enter",
task_id=self.task_id,
agent_id=self.agent_id,
display_name=self.display_name,
multi_agent_mode=self.multi_agent_mode,
has_state=bool(self.multi_agent_state),
state_id=id(self.multi_agent_state) if self.multi_agent_state else None,
output_len=len(output_text),
is_final=is_final,
)
if not self.multi_agent_state: if not self.multi_agent_state:
ma_debug("sub_agent_forward_no_state", task_id=self.task_id, agent_id=self.agent_id)
return return
# 如果这是对 pending 提问的回答,不走主对话转发,而是返回到 ask 工具结果 # 如果这是对 pending 提问的回答,不走主对话转发,而是返回到 ask 工具结果
if self._provide_answer(output_text): if self._provide_answer(output_text):
@ -281,6 +303,13 @@ class SubAgentTask:
from modules.multi_agent.state import build_sub_agent_output_text from modules.multi_agent.state import build_sub_agent_output_text
msg = build_sub_agent_output_text(self.display_name, output_text.strip(), is_final=is_final) msg = build_sub_agent_output_text(self.display_name, output_text.strip(), is_final=is_final)
self.multi_agent_state.push_master_message(msg) self.multi_agent_state.push_master_message(msg)
ma_debug(
"sub_agent_forward_pushed",
task_id=self.task_id,
agent_id=self.agent_id,
state_id=id(self.multi_agent_state) if self.multi_agent_state else None,
msg_preview=msg[:200],
)
# 同时记录到实例状态,供 list_active_sub_agents 使用 # 同时记录到实例状态,供 list_active_sub_agents 使用
inst = self.multi_agent_state.get_instance(self.agent_id) inst = self.multi_agent_state.get_instance(self.agent_id)
if inst: if inst:

View File

@ -5,3 +5,5 @@
{agents_list} {agents_list}
创建子智能体时请使用 `create_sub_agent` 工具,并传入合适的 `role_id`。 创建子智能体时请使用 `create_sub_agent` 工具,并传入合适的 `role_id`。
如果没有创建新智能体无需使用list_agents工具直接根据现有的子智能体来创建即可。

View File

@ -131,7 +131,13 @@ from .chat_flow_runtime import (
detect_malformed_tool_call, detect_malformed_tool_call,
) )
from .chat_flow_task_support import process_sub_agent_updates, process_background_command_updates, process_multi_agent_master_messages from .chat_flow_task_support import (
process_sub_agent_updates,
process_background_command_updates,
process_multi_agent_master_messages,
inject_multi_agent_master_message,
_auto_message_type_for_multi_agent_subtype,
)
from .chat_flow_tool_loop import execute_tool_calls from .chat_flow_tool_loop import execute_tool_calls
from .chat_flow_stream_loop import run_streaming_attempts from .chat_flow_stream_loop import run_streaming_attempts
from .deep_compression import run_deep_compression from .deep_compression import run_deep_compression
@ -791,43 +797,21 @@ def _collect_pending_completion_notices(*, web_terminal, conversation_id: str) -
"sort_key": update.get("updated_at") or time.time(), "sort_key": update.get("updated_at") or time.time(),
}) })
# 3) 多智能体模式:把子智能体转发到主对话的 pending 消息也作为通知池项消费
if getattr(web_terminal, "multi_agent_mode", False):
sub_manager = getattr(web_terminal, "sub_agent_manager", None)
if sub_manager:
state = sub_manager.get_multi_agent_state(conversation_id)
if state:
ma_messages = state.drain_master_messages()
ma_debug(
"collect_notices_drain_ma_messages",
conversation_id=conversation_id,
count=len(ma_messages),
previews=[str(m)[:200] for m in ma_messages],
)
for msg_text in ma_messages:
notices.append({
"kind": "multi_agent",
"message": msg_text,
"payload": {
"sub_agent_notice": True,
"message_source": "sub_agent",
"multi_agent_output": True,
},
"sort_key": time.time(),
})
notices.sort(key=lambda item: item.get("sort_key") or 0) notices.sort(key=lambda item: item.get("sort_key") or 0)
return notices return notices
def _has_pending_completion_work(*, web_terminal, conversation_id: str) -> bool: def _has_pending_completion_work(*, web_terminal, conversation_id: str) -> bool:
"""是否还有运行中或待通知的后台任务(用于决定是否继续轮询)。""" """是否还有运行中或待通知的传统后台任务(子智能体/后台 run_command
多智能体模式下的 pending 消息由独立的 poll_multi_agent_notifications 处理
不再混入本函数
"""
sub_manager = getattr(web_terminal, "sub_agent_manager", None) sub_manager = getattr(web_terminal, "sub_agent_manager", None)
if sub_manager: if sub_manager:
announced = getattr(web_terminal, "_announced_sub_agent_tasks", set()) announced = getattr(web_terminal, "_announced_sub_agent_tasks", set())
has_running_non_ma = False has_running_non_ma = False
has_unnotified_non_ma = False has_unnotified_non_ma = False
has_running_ma = False
for task in sub_manager.tasks.values(): for task in sub_manager.tasks.values():
if not isinstance(task, dict): if not isinstance(task, dict):
continue continue
@ -836,26 +820,13 @@ def _has_pending_completion_work(*, web_terminal, conversation_id: str) -> bool:
status = task.get("status") status = task.get("status")
multi_agent_flag = task.get("multi_agent_mode") or False multi_agent_flag = task.get("multi_agent_mode") or False
if status not in TERMINAL_STATUSES.union({"terminated"}): if status not in TERMINAL_STATUSES.union({"terminated"}):
if multi_agent_flag: if not multi_agent_flag and task.get("run_in_background"):
has_running_ma = True
elif task.get("run_in_background"):
has_running_non_ma = True has_running_non_ma = True
continue continue
if not multi_agent_flag and task.get("run_in_background") and (task.get("task_id") not in announced) and not task.get("notified"): if not multi_agent_flag and task.get("run_in_background") and (task.get("task_id") not in announced) and not task.get("notified"):
has_unnotified_non_ma = True has_unnotified_non_ma = True
if has_running_non_ma or has_unnotified_non_ma: if has_running_non_ma or has_unnotified_non_ma:
return True return True
# 多智能体模式:只要有未消费的主对话消息,或还有未结束(非 idle/terminated的实例就继续轮询。
# 即使全部实例都已 idle只要 pending 消息尚未消费,也要继续,避免主任务结束后通知丢失。
if getattr(web_terminal, "multi_agent_mode", False):
state = sub_manager.get_multi_agent_state(conversation_id)
if state:
if state.has_pending_master_messages():
return True
if any(a.status not in {"idle", "terminated"} for a in state.list_all()):
return True
return False
return True
bg_manager = getattr(web_terminal, "background_command_manager", None) bg_manager = getattr(web_terminal, "background_command_manager", None)
if bg_manager: if bg_manager:
try: try:
@ -980,6 +951,364 @@ async def poll_completion_notifications(*, web_terminal, workspace, conversation
await asyncio.sleep(5) await asyncio.sleep(5)
async def poll_multi_agent_notifications(*, web_terminal, workspace, conversation_id, client_sid, username):
"""多智能体模式专用通知池。
只在主任务结束后启动负责两件事
1. 若主任务已空闲且 MultiAgentState 中有 pending_master_messages
一次性取出并触发新一轮主任务工作即使仍有 running 子智能体也立即派发
主对话空闲时消息池有消息就该全部插入设计中属情况2
2. pending 为空且仍有 running 实例循环等待下一次 push 新输出
重要旧实现要求所有 running 实例退出再 drain导致 ask_master 阻塞中的
子智能体永远等不到主对话回答而超时现已修复为池优先
poll_completion_notifications 完全分离避免多智能体消息和传统后台
通知竞争 task_manager 的单工作区互斥
"""
from .extensions import socketio
max_wait_time = 3600
start_wait = time.time()
def sender(event_type, data):
try:
socketio.emit(event_type, data, room=f"user_{username}")
except Exception:
pass
import threading as _threading
ma_debug(
"poll_ma_notifications_start",
conversation_id=conversation_id,
thread_id=_threading.get_ident(),
multi_agent_mode=getattr(web_terminal, "multi_agent_mode", False),
main_task_active_at_start=getattr(web_terminal, "_multi_agent_main_task_active", False),
)
tick_count = 0
while (time.time() - start_wait) < max_wait_time:
tick_count += 1
# 检查停止标志
client_stop_info = get_stop_flag(client_sid, username)
if client_stop_info:
stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
if stop_requested:
ma_debug("poll_ma_notifications_stop_requested", conversation_id=conversation_id, tick=tick_count)
break
manager = getattr(web_terminal, "sub_agent_manager", None)
if not manager:
ma_debug("poll_ma_no_manager", conversation_id=conversation_id, tick=tick_count)
break
state = manager.get_multi_agent_state(conversation_id)
if not state:
ma_debug("poll_ma_no_state", conversation_id=conversation_id, tick=tick_count)
break
# 主任务又被唤醒了(用户发送新消息等),让主循环自己处理,本通知池退出
if getattr(web_terminal, "_multi_agent_main_task_active", False):
ma_debug("poll_ma_notifications_main_task_active", conversation_id=conversation_id, tick=tick_count)
break
# 状态快照
all_insts = state.list_all()
statuses = [a.status for a in all_insts]
pending_count_now = len(state.pending_master_messages)
main_active = getattr(web_terminal, "_multi_agent_main_task_active", False)
ma_debug(
"poll_ma_tick",
conversation_id=conversation_id,
tick=tick_count,
instance_count=len(all_insts),
statuses=statuses,
pending_count=pending_count_now,
main_active=main_active,
)
# 1) 先检查 pending 池——主对话空闲时只要池里有消息就立即派发情况2
if state.has_pending_master_messages():
pending = state.drain_master_messages()
ma_debug(
"poll_ma_notifications_dispatch",
conversation_id=conversation_id,
count=len(pending),
running_count=len([a for a in all_insts if a.status == "running"]),
previews=[str(m)[:200] for m in pending],
tick=tick_count,
)
try:
await _dispatch_multi_agent_idle_messages(
web_terminal=web_terminal,
workspace=workspace,
sender=sender,
client_sid=client_sid,
username=username,
conversation_id=conversation_id,
messages=pending,
)
ma_debug("poll_ma_dispatch_success", conversation_id=conversation_id, count=len(pending), tick=tick_count)
except Exception as exc:
debug_log(f"[MultiAgent] 分发 idle 消息失败: {exc}")
ma_debug(
"poll_ma_notifications_dispatch_error",
conversation_id=conversation_id,
error=str(exc),
error_type=type(exc).__name__,
tick=tick_count,
)
# 分发后退出,由新任务结束后再决定是否继续轮询
return
# 2) 没有 pending 消息:检查还有哪些子智能体在运行,决定退出还是继续等
running_agents = [a for a in all_insts if a.status == "running"]
if not running_agents:
ma_debug("poll_ma_notifications_no_work_end", conversation_id=conversation_id, tick=tick_count)
break
ma_debug(
"poll_ma_notifications_wait_running",
conversation_id=conversation_id,
running_count=len(running_agents),
tick=tick_count,
)
await asyncio.sleep(0.5)
ma_debug("poll_ma_notifications_end", conversation_id=conversation_id, tick=tick_count)
async def _dispatch_multi_agent_idle_messages(
*,
web_terminal,
workspace,
sender,
client_sid,
username,
conversation_id,
messages: List[str],
):
"""主智能体空闲时,把多智能体 pending 消息作为新一轮用户消息分发。
所有消息先持久化到对话历史 N-1 条作为前置通知通过 socketio 推送
最后一条创建 task_type="notice" 任务触发 Team Leader 新一轮工作
"""
ma_debug(
"dispatch_ma_idle_enter",
conversation_id=conversation_id,
count=len(messages) if messages else 0,
previews=[str(m)[:150] for m in (messages or [])],
)
if not messages:
ma_debug("dispatch_ma_idle_no_messages", conversation_id=conversation_id)
return
from modules.multi_agent.state import parse_multi_agent_message
from .tasks import task_manager
parsed_messages = []
for msg_text in messages:
parsed = parse_multi_agent_message(msg_text) or {}
parsed_messages.append({
"text": msg_text,
"display_name": parsed.get("display_name"),
"subtype": parsed.get("subtype"),
})
# 1) 前置通知全部写入历史 + emit给在线客户端未后一条会由后续任务自己持久化
# 避免重复产生两条相同 user 消息导致刷新后被渲染两遍)。
ma_debug(
"dispatch_ma_idle_preceding",
conversation_id=conversation_id,
preceding_count=max(0, len(parsed_messages) - 1),
)
for item in parsed_messages[:-1]:
inject_multi_agent_master_message(
web_terminal=web_terminal,
messages=None,
text=item["text"],
sender=sender,
conversation_id=conversation_id,
inline=False,
)
# 1.5) 最后一条只 emit 给在线客户端,不在这里持久化(后续 task handle_task_with_sender
# 会以这条文本作为 message 创建任务,并写入历史,从而避免重复写入。)
last = parsed_messages[-1]
message_source = "sub_agent"
last_emit_payload = {
"message": last["text"],
"content": last["text"],
"conversation_id": conversation_id,
"message_source": message_source,
"sub_agent_notice": True,
"multi_agent_message": True,
"multi_agent_display_name": last["display_name"],
"multi_agent_subtype": last["subtype"],
"auto_message_type": _auto_message_type_for_multi_agent_subtype(last["subtype"]),
"visibility": "chat",
"starts_work": False,
"metadata": {
"message_source": message_source,
"auto_message_type": _auto_message_type_for_multi_agent_subtype(last["subtype"]),
"multi_agent_message": True,
"multi_agent_display_name": last["display_name"],
"multi_agent_subtype": last["subtype"],
"visibility": "chat",
"starts_work": False,
},
"timestamp": datetime.now().isoformat(),
}
try:
sender("user_message", last_emit_payload)
ma_debug(
"dispatch_ma_idle_last_emit",
conversation_id=conversation_id,
message_preview=last["text"][:200],
)
except Exception as exc:
ma_debug(
"dispatch_ma_idle_last_emit_error",
conversation_id=conversation_id,
error=str(exc),
)
# 2) 前置通知的 socketio emit 已在上述注入 inject_multi_agent_master_message 内
# 同时完成,避免在多智能体消息场景重复发送相同事件。
# 3) 最后一条触发新一轮工作
ui_defaults = dict(_user_message_ui_defaults(message_source, auto_user_message_event=True))
ui_defaults["starts_work"] = False
workspace_id = getattr(workspace, "workspace_id", None) or "default"
host_mode = bool(getattr(workspace, "username", None) == "host")
session_data = {
"username": username,
"role": getattr(web_terminal, "user_role", "user"),
"is_api_user": getattr(web_terminal, "user_role", "") == "api",
"host_mode": host_mode,
"host_workspace_id": workspace_id if host_mode else None,
"workspace_id": workspace_id,
"run_mode": getattr(web_terminal, "run_mode", None),
"thinking_mode": getattr(web_terminal, "thinking_mode", None),
"model_key": getattr(web_terminal, "model_key", None),
"message_source": message_source,
}
ma_auto_type = _auto_message_type_for_multi_agent_subtype(last["subtype"])
# 重要ui_defaults 默认会给出 visibility="compact"/starts_work=False
# 多智能体主消息是正常聊天消息,必须在 **ui_defaults 之后覆写为 chat
# 否则后端历史 metadata.visibility=compact 会让加载的消息走通知渲染。
session_data["auto_user_message_event"] = True
session_data["auto_user_message_payload"] = {
**ui_defaults,
"message_source": message_source,
"sub_agent_notice": True,
"multi_agent_message": True,
"multi_agent_display_name": last["display_name"],
"multi_agent_subtype": last["subtype"],
"auto_message_type": ma_auto_type,
"starts_work": False,
"visibility": "chat",
"timestamp": datetime.now().isoformat(),
}
ma_debug(
"dispatch_ma_idle_before_create_task",
conversation_id=conversation_id,
last_text_preview=last["text"][:300],
workspace_id=workspace_id,
username=username,
)
session_data["auto_user_message_payload"].setdefault("metadata", {
**ui_defaults,
"message_source": message_source,
"auto_message_type": ma_auto_type,
"multi_agent_message": True,
"multi_agent_display_name": last["display_name"],
"multi_agent_subtype": last["subtype"],
"starts_work": False,
"visibility": "chat",
})
if len(parsed_messages) > 1:
session_data["preceding_user_notices"] = [
{
"message": item["text"],
"payload": {
"message_source": "sub_agent",
"sub_agent_notice": True,
"multi_agent_message": True,
"multi_agent_display_name": item["display_name"],
"multi_agent_subtype": item["subtype"],
"auto_message_type": _auto_message_type_for_multi_agent_subtype(item["subtype"]),
"visibility": "chat",
"starts_work": False,
},
}
for item in parsed_messages[:-1]
]
try:
rec = task_manager.create_chat_task(
username,
workspace_id,
last["text"],
[],
conversation_id,
model_key=session_data.get("model_key"),
thinking_mode=session_data.get("thinking_mode"),
run_mode=session_data.get("run_mode"),
session_data=session_data,
task_type="notice",
)
except Exception as exc:
ma_debug(
"dispatch_ma_idle_create_task_exception",
conversation_id=conversation_id,
error=str(exc),
error_type=type(exc).__name__,
)
raise
task_id_created = getattr(rec, "task_id", None)
ma_debug("dispatch_ma_idle_task_created", conversation_id=conversation_id, task_id=task_id_created)
payload = {
"message": last["text"],
"content": last["text"],
"conversation_id": conversation_id,
"task_id": getattr(rec, "task_id", None),
"message_source": message_source,
"sub_agent_notice": True,
"multi_agent_message": True,
"multi_agent_display_name": last["display_name"],
"multi_agent_subtype": last["subtype"],
"visibility": "chat",
"starts_work": False,
"auto_message_type": _auto_message_type_for_multi_agent_subtype(last["subtype"]),
"metadata": {
"message_source": message_source,
"auto_message_type": _auto_message_type_for_multi_agent_subtype(last["subtype"]),
"multi_agent_message": True,
"multi_agent_display_name": last["display_name"],
"multi_agent_subtype": last["subtype"],
"visibility": "chat",
"starts_work": False,
},
"timestamp": datetime.now().isoformat(),
}
sender("user_message", payload)
ma_debug(
"dispatch_ma_idle_sender_user_message",
conversation_id=conversation_id,
task_id=getattr(rec, "task_id", None),
payload_message_preview=last["text"][:200],
)
ma_debug(
"dispatch_ma_idle_exit_ok",
conversation_id=conversation_id,
count=len(parsed_messages),
task_id=getattr(rec, "task_id", None),
)
async def handle_task_with_sender( async def handle_task_with_sender(
terminal: WebTerminal, terminal: WebTerminal,
workspace: UserWorkspace, workspace: UserWorkspace,
@ -1008,6 +1337,8 @@ async def handle_task_with_sender(
ma_debug( ma_debug(
"handle_task_with_sender_start", "handle_task_with_sender_start",
conversation_id=conversation_id, conversation_id=conversation_id,
terminal_id=id(web_terminal),
manager_id=id(manager) if manager else None,
message_preview=str(message)[:300] if message else None, message_preview=str(message)[:300] if message else None,
pending_master_messages_count=pending_count, pending_master_messages_count=pending_count,
) )
@ -1058,6 +1389,7 @@ async def handle_task_with_sender(
# 构建 user 消息来源与 metadata # 构建 user 消息来源与 metadata
source_from_terminal = str(getattr(web_terminal, "_current_user_message_source", "user") or "user").strip().lower() source_from_terminal = str(getattr(web_terminal, "_current_user_message_source", "user") or "user").strip().lower()
user_message_source = source_from_terminal or "user" user_message_source = source_from_terminal or "user"
multi_agent_meta: Dict[str, Any] = {}
if auto_user_message_event: if auto_user_message_event:
auto_payload = getattr(web_terminal, "_auto_user_message_payload", None) auto_payload = getattr(web_terminal, "_auto_user_message_payload", None)
if isinstance(auto_payload, dict): if isinstance(auto_payload, dict):
@ -1072,6 +1404,11 @@ async def handle_task_with_sender(
user_message_source = "background_command" user_message_source = "background_command"
elif auto_payload.get("sub_agent_notice"): elif auto_payload.get("sub_agent_notice"):
user_message_source = "sub_agent" user_message_source = "sub_agent"
# 保留多智能体元数据,避免前端把子智能体输出渲染成普通 user 消息
if auto_payload.get("multi_agent_message"):
multi_agent_meta["multi_agent_message"] = True
multi_agent_meta["multi_agent_display_name"] = auto_payload.get("multi_agent_display_name")
multi_agent_meta["multi_agent_subtype"] = auto_payload.get("multi_agent_subtype")
if user_message_source not in _VALID_USER_MESSAGE_SOURCES: if user_message_source not in _VALID_USER_MESSAGE_SOURCES:
user_message_source = "user" user_message_source = "user"
@ -1093,6 +1430,20 @@ async def handle_task_with_sender(
# 如果是自动发送的user消息子智能体/后台命令完成通知),添加标记 # 如果是自动发送的user消息子智能体/后台命令完成通知),添加标记
if auto_user_message_event: if auto_user_message_event:
user_message_metadata["is_auto_generated"] = True user_message_metadata["is_auto_generated"] = True
if multi_agent_meta.get("multi_agent_message"):
# 多智能体消息:恢复专用 auto_message_type 与显示字段
user_message_metadata["auto_message_type"] = _auto_message_type_for_multi_agent_subtype(
multi_agent_meta.get("multi_agent_subtype")
)
user_message_metadata.update(multi_agent_meta)
# 多智能体消息是正常聊天消息,走多智能体动态专用渲染,
# 不应走 _user_message_ui_defaults 默认给出的 visibility="compact"(通知渲染)。
user_message_metadata["visibility"] = "chat"
# 多智能体消息触发的是主智能体对子智能体输出的续答,不应显示新的
# assistant 回复头部Astrion/工作时间),因此 starts_work=false。
# 前端通过单独的 multi_agent_message 标记恢复轮询。
user_message_metadata["starts_work"] = False
else:
user_message_metadata["auto_message_type"] = "completion_notice" user_message_metadata["auto_message_type"] = "completion_notice"
saved_user_message = web_terminal.context_manager.add_conversation( saved_user_message = web_terminal.context_manager.add_conversation(
"user", "user",
@ -1892,19 +2243,15 @@ async def handle_task_with_sender(
if not hasattr(web_terminal, "_announced_sub_agent_tasks"): if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
web_terminal._announced_sub_agent_tasks = set() web_terminal._announced_sub_agent_tasks = set()
# 多智能体模式:检测是否还有运行中/idle 的多智能体实例。 # 多智能体模式:检测是否还有真正运行中的实例,以及是否有尚未消费的 pending 消息。
# 它们不是传统后台任务,不阻塞前端输入区,但需要通知池在主任务结束后 # idle 实例已完成当前输出、保留上下文等待后续指令pending 消息会在主任务
# 继续消费 pending 消息并触发 Team Leader 响应。 # 结束后由 poll_multi_agent_notifications 继续消费并触发新一轮工作。
has_pending_ma_messages = False
if getattr(web_terminal, "multi_agent_mode", False): if getattr(web_terminal, "multi_agent_mode", False):
for task in manager.tasks.values(): state = manager.get_multi_agent_state(conversation_id)
if ( if state:
isinstance(task, dict) has_running_multi_agent = any(a.status == "running" for a in state.list_all())
and task.get("conversation_id") == conversation_id has_pending_ma_messages = state.has_pending_master_messages()
and task.get("multi_agent_mode")
and task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
):
has_running_multi_agent = True
break
running_tasks = [ running_tasks = [
task for task in manager.tasks.values() task for task in manager.tasks.values()
@ -1944,17 +2291,20 @@ async def handle_task_with_sender(
# 与子智能体完全复用同一 waiting 事件(前端已有稳定处理链路) # 与子智能体完全复用同一 waiting 事件(前端已有稳定处理链路)
sender('sub_agent_waiting', _build_shared_waiting_payload(waiting_items)) sender('sub_agent_waiting', _build_shared_waiting_payload(waiting_items))
# 统一「通知池」轮询器:子智能体 + 后台 run_command + 多智能体 pending 消息 # 传统后台任务通知池:只处理 run_in_background=True 的子智能体 / 后台 run_command。
# 合并为单一轮询链路,每轮一次性取出所有待通知项,避免逐条触发「工作 → 停止 → 再工作」循环。 needs_completion_poll = (
# 只 spawn 一个轮询器(无论是否同时存在子智能体与后台命令),从根本上消除 has_running_sub_agents
# 两个轮询器同时 create_chat_task 撞「单工作区互斥」的问题。 or has_running_background_commands
if has_running_sub_agents or has_running_background_commands or has_running_multi_agent: or _has_pending_completion_work(
web_terminal=web_terminal, conversation_id=conversation_id
)
)
if needs_completion_poll:
ma_debug( ma_debug(
"completion_poll_start", "completion_poll_start",
conversation_id=conversation_id, conversation_id=conversation_id,
has_running_sub_agents=has_running_sub_agents, has_running_sub_agents=has_running_sub_agents,
has_running_background_commands=has_running_background_commands, has_running_background_commands=has_running_background_commands,
has_running_multi_agent=has_running_multi_agent,
) )
def run_completion_poll(): def run_completion_poll():
import asyncio import asyncio
@ -1972,7 +2322,60 @@ async def handle_task_with_sender(
loop.close() loop.close()
socketio.start_background_task(run_completion_poll) socketio.start_background_task(run_completion_poll)
# 多智能体模式独立通知池:处理 running 实例 / idle 实例待消费的 pending 消息。
# 与传统后台任务完全分离,避免两者竞争 create_chat_task 的单工作区互斥。
ma_mode_flag = bool(getattr(web_terminal, "multi_agent_mode", False))
needs_ma_poll = ma_mode_flag and (has_running_multi_agent or has_pending_ma_messages)
ma_debug(
"ma_poll_decision",
conversation_id=conversation_id,
multi_agent_mode=ma_mode_flag,
has_running_multi_agent=has_running_multi_agent,
has_pending_ma_messages=has_pending_ma_messages,
needs_ma_poll=needs_ma_poll,
)
if needs_ma_poll:
ma_debug(
"ma_poll_start",
conversation_id=conversation_id,
has_running_multi_agent=has_running_multi_agent,
has_pending_ma_messages=has_pending_ma_messages,
)
def run_ma_poll():
import asyncio
import threading as _t
ma_debug(
"ma_poll_thread_enter",
conversation_id=conversation_id,
thread_id=_t.get_ident(),
)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(poll_multi_agent_notifications(
web_terminal=web_terminal,
workspace=workspace,
conversation_id=conversation_id,
client_sid=client_sid,
username=username
))
except Exception as exc:
ma_debug(
"ma_poll_thread_exception",
conversation_id=conversation_id,
error=str(exc),
error_type=type(exc).__name__,
)
else: else:
ma_debug("ma_poll_thread_exit_ok", conversation_id=conversation_id)
finally:
loop.close()
ma_debug("ma_poll_thread_finally", conversation_id=conversation_id)
socketio.start_background_task(run_ma_poll)
if not needs_completion_poll and not needs_ma_poll:
ma_debug( ma_debug(
"completion_poll_skip", "completion_poll_skip",
conversation_id=conversation_id, conversation_id=conversation_id,
@ -2049,5 +2452,8 @@ async def handle_task_with_sender(
# 沿用子智能体字段,确保前端直接走已验证通路 # 沿用子智能体字段,确保前端直接走已验证通路
'has_running_sub_agents': has_running_completion_jobs, 'has_running_sub_agents': has_running_completion_jobs,
'has_running_background_commands': has_running_background_commands, 'has_running_background_commands': has_running_background_commands,
# 多智能体模式:只要还有 running 实例或尚未消费的 pending 消息,
# 前端就应保持运行态并继续轮询。
'has_running_multi_agent': has_running_multi_agent or has_pending_ma_messages,
'pending_runtime_guidance_messages': pending_runtime_guidance_messages, 'pending_runtime_guidance_messages': pending_runtime_guidance_messages,
}) })

View File

@ -378,8 +378,13 @@ def inject_multi_agent_master_message(
ctx_manager = getattr(web_terminal, "context_manager", None) ctx_manager = getattr(web_terminal, "context_manager", None)
if ctx_manager is not None: if ctx_manager is not None:
ctx_manager.add_conversation("user", raw, metadata=metadata) ctx_manager.add_conversation("user", raw, metadata=metadata)
except Exception: ma_debug(
pass "inject_ma_message_saved",
conversation_id=conversation_id,
text_preview=raw[:200],
)
except Exception as exc:
ma_debug("inject_ma_message_save_error", error=str(exc))
if messages is not None: if messages is not None:
insert_index = len(messages) insert_index = len(messages)
@ -392,6 +397,13 @@ def inject_multi_agent_master_message(
insert_index = end insert_index = end
break break
messages.insert(insert_index, {"role": "user", "content": raw}) messages.insert(insert_index, {"role": "user", "content": raw})
ma_debug(
"inject_ma_message_inserted",
conversation_id=conversation_id,
insert_index=insert_index,
messages_len_after=len(messages),
text_preview=raw[:200],
)
if callable(sender): if callable(sender):
payload = { payload = {
@ -428,19 +440,35 @@ async def process_multi_agent_master_messages(
after_tool_call_id: Optional[str] = None, after_tool_call_id: Optional[str] = None,
) -> int: ) -> int:
"""从 MultiAgentState 取出待插入主对话的消息并注入。返回注入条数。""" """从 MultiAgentState 取出待插入主对话的消息并注入。返回注入条数。"""
manager = getattr(web_terminal, "sub_agent_manager", None)
ma_debug(
"process_ma_messages_enter",
multi_agent_mode=getattr(web_terminal, "multi_agent_mode", False),
has_manager=bool(manager),
manager_id=id(manager) if manager else None,
conversation_id=getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None),
inline=inline,
after_tool_call_id=after_tool_call_id,
messages_len_before=len(messages) if messages else 0,
)
if not getattr(web_terminal, "multi_agent_mode", False): if not getattr(web_terminal, "multi_agent_mode", False):
return 0 return 0
manager = getattr(web_terminal, "sub_agent_manager", None) manager = getattr(web_terminal, "sub_agent_manager", None)
if not manager: if not manager:
ma_debug("process_ma_messages_no_manager")
return 0 return 0
conversation_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None) conversation_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None)
if not conversation_id: if not conversation_id:
ma_debug("process_ma_messages_no_conversation_id")
return 0 return 0
state = manager.get_multi_agent_state(conversation_id) state = manager.get_multi_agent_state(conversation_id)
if not state: if not state:
ma_debug("process_ma_messages_no_state", conversation_id=conversation_id)
return 0 return 0
ma_debug("process_ma_messages_got_state", conversation_id=conversation_id, state_id=id(state))
pending = state.drain_master_messages() pending = state.drain_master_messages()
if not pending: if not pending:
ma_debug("process_ma_messages_empty", conversation_id=conversation_id, state_id=id(state))
return 0 return 0
debug_log(f"[MultiAgent] draining {len(pending)} pending master messages") debug_log(f"[MultiAgent] draining {len(pending)} pending master messages")
ma_debug( ma_debug(

View File

@ -83,6 +83,7 @@ def _task_public_payload(rec: TaskRecord, *, include_title: bool = True) -> Dict
"message_source": (rec.session_data or {}).get("message_source"), "message_source": (rec.session_data or {}).get("message_source"),
"goal_mode": bool((rec.session_data or {}).get("goal_mode")), "goal_mode": bool((rec.session_data or {}).get("goal_mode")),
"goal_progress": (rec.session_data or {}).get("goal_progress"), "goal_progress": (rec.session_data or {}).get("goal_progress"),
"task_type": getattr(rec, "task_type", "chat"),
} }
if include_title: if include_title:
payload["conversation_title"] = _conversation_title_for_task(rec) payload["conversation_title"] = _conversation_title_for_task(rec)

View File

@ -55,6 +55,7 @@ class TaskRecord:
"runtime_pending_queue", "runtime_pending_queue",
"runtime_guidance_queue", "runtime_guidance_queue",
"last_cancel_at", "last_cancel_at",
"task_type",
) )
def __init__( def __init__(
@ -68,6 +69,7 @@ class TaskRecord:
thinking_mode: Optional[bool], thinking_mode: Optional[bool],
run_mode: Optional[str], run_mode: Optional[str],
max_iterations: Optional[int], max_iterations: Optional[int],
task_type: str = "chat",
): ):
self.task_id = task_id self.task_id = task_id
self.username = username self.username = username
@ -92,6 +94,7 @@ class TaskRecord:
self.runtime_pending_queue: List[Dict[str, Any]] = [] self.runtime_pending_queue: List[Dict[str, Any]] = []
self.runtime_guidance_queue: List[str] = [] self.runtime_guidance_queue: List[str] = []
self.last_cancel_at: Optional[float] = None self.last_cancel_at: Optional[float] = None
self.task_type = task_type
class TaskManager: class TaskManager:
"""线程内存版任务管理器,后续可替换为 Redis/DB。""" """线程内存版任务管理器,后续可替换为 Redis/DB。"""
@ -133,18 +136,22 @@ class TaskManager:
message_source: Optional[str] = None, message_source: Optional[str] = None,
goal_mode: bool = False, goal_mode: bool = False,
skill_context_messages: Optional[List[Dict[str, str]]] = None, skill_context_messages: Optional[List[Dict[str, str]]] = None,
task_type: str = "chat",
) -> TaskRecord: ) -> TaskRecord:
if run_mode: if run_mode:
normalized = str(run_mode).lower() normalized = str(run_mode).lower()
if normalized not in {"fast", "thinking", "deep"}: if normalized not in {"fast", "thinking", "deep"}:
raise ValueError("run_mode 只支持 fast/thinking/deep") raise ValueError("run_mode 只支持 fast/thinking/deep")
run_mode = normalized run_mode = normalized
# 单工作区互斥:禁止同一用户同一工作区并发任务 normalized_task_type = str(task_type or "chat").strip().lower() or "chat"
existing = [t for t in self.list_tasks(username, workspace_id) if t.status in {"pending", "running"}] # 单工作区互斥:普通 chat 任务禁止同一用户同一工作区并发;
# notice通知触发任务允许与已完成的 chat 任务共存,用于后台通知重入。
if normalized_task_type == "chat":
existing = [t for t in self.list_tasks(username, workspace_id) if t.status in {"pending", "running"} and getattr(t, "task_type", "chat") == "chat"]
if existing: if existing:
raise RuntimeError("当前工作区已有运行中的任务,请稍后再试。") raise RuntimeError("当前工作区已有运行中的任务,请稍后再试。")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
record = TaskRecord(task_id, username, workspace_id, message, conversation_id, model_key, thinking_mode, run_mode, max_iterations) record = TaskRecord(task_id, username, workspace_id, message, conversation_id, model_key, thinking_mode, run_mode, max_iterations, task_type=normalized_task_type)
# 记录当前 session 快照,便于后台线程内使用 # 记录当前 session 快照,便于后台线程内使用
if session_data is not None: if session_data is not None:
snapshot = dict(session_data) snapshot = dict(session_data)

View File

@ -25,6 +25,8 @@ export const traceLog = (...args) => {
function isGoalModeDebugEnabled() { function isGoalModeDebugEnabled() {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
try { try {
// 临时强制开启,用于调试多智能体消息轮询问题
return true;
const explicit = (window as any).__GOAL_MODE_DEBUG__; const explicit = (window as any).__GOAL_MODE_DEBUG__;
if (explicit === true || explicit === '1') return true; if (explicit === true || explicit === '1') return true;
if (explicit === false || explicit === '0') return false; if (explicit === false || explicit === '0') return false;

View File

@ -372,8 +372,9 @@ export const lifecycleMethods = {
}); });
const hasRunningSubAgents = !!data?.has_running_sub_agents; const hasRunningSubAgents = !!data?.has_running_sub_agents;
const hasRunningBackgroundCommands = !!data?.has_running_background_commands; const hasRunningBackgroundCommands = !!data?.has_running_background_commands;
if (hasRunningSubAgents) { const hasRunningMultiAgent = !!data?.has_running_multi_agent;
debugLog('[TaskPolling] 任务完成,但仍有后台子智能体运行'); if (hasRunningSubAgents || hasRunningMultiAgent) {
debugLog('[TaskPolling] 任务完成,但仍有后台子智能体/多智能体运行');
} else { } else {
debugLog('[TaskPolling] 任务完成'); debugLog('[TaskPolling] 任务完成');
} }
@ -381,11 +382,23 @@ export const lifecycleMethods = {
// 同步处理状态更新 // 同步处理状态更新
this.streamingMessage = false; this.streamingMessage = false;
this.stopRequested = false; this.stopRequested = false;
if (!hasRunningSubAgents) { if (!hasRunningSubAgents && !hasRunningMultiAgent) {
this.markLatestUserWorkCompleted(); this.markLatestUserWorkCompleted();
} }
if (hasRunningSubAgents) { if (hasRunningMultiAgent) {
// 多智能体模式下主智能体已空闲,但仍有实例在运行,保持对话运行态继续接收后续消息;
// 不启动后台等待轮询也不阻塞输入区waitingForSubAgent=false
// 但必须启动运行中任务探测,否则子智能体后续输出触发的新主任务无法被前端发现。
jsonDebug('handleTaskComplete:hasRunningMultiAgent', {
taskInProgress: this.taskInProgress,
currentConversationId: this.currentConversationId
});
this.taskInProgress = true;
this.waitingForSubAgent = false;
this.waitingForBackgroundCommand = hasRunningBackgroundCommands;
this.startMultiAgentTaskProbe();
} else if (hasRunningSubAgents) {
this.taskInProgress = true; this.taskInProgress = true;
this.waitingForSubAgent = true; this.waitingForSubAgent = true;
this.waitingForBackgroundCommand = hasRunningBackgroundCommands; this.waitingForBackgroundCommand = hasRunningBackgroundCommands;
@ -403,6 +416,7 @@ export const lifecycleMethods = {
this.waitingForSubAgent = false; this.waitingForSubAgent = false;
this.waitingForBackgroundCommand = false; this.waitingForBackgroundCommand = false;
this.stopWaitingTaskProbe(); this.stopWaitingTaskProbe();
this.stopMultiAgentTaskProbe();
this.clearTaskState(); // 清理任务状态 this.clearTaskState(); // 清理任务状态
this.$nextTick(() => { this.$nextTick(() => {
if (typeof this.tryAutoSendRuntimeQueuedMessages === 'function') { if (typeof this.tryAutoSendRuntimeQueuedMessages === 'function') {

View File

@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import { debugLog } from '../common'; import { debugLog, goalModeDebugLog } from '../common';
import { useTaskStore } from '../../../stores/task'; import { useTaskStore } from '../../../stores/task';
import { getMessageVisibility, messageStartsWork } from '../../../utils/messageVisibility'; import { getMessageVisibility, messageStartsWork } from '../../../utils/messageVisibility';
import { import {
@ -86,6 +86,132 @@ export const probeMethods = {
} }
}, 1000); }, 1000);
}, },
// ---------- 多智能体任务探测 ----------
stopMultiAgentTaskProbe() {
if (this.multiAgentTaskProbeTimer) {
debugNotifyLog('[DEBUG_NOTIFY][ui] stopMultiAgentTaskProbe');
goalModeDebugLog('probe.stopMultiAgentTaskProbe', {
conversationId: this.currentConversationId,
taskInProgress: this.taskInProgress
});
clearInterval(this.multiAgentTaskProbeTimer);
this.multiAgentTaskProbeTimer = null;
}
},
startMultiAgentTaskProbe() {
if (this.multiAgentTaskProbeTimer) {
debugNotifyLog('[DEBUG_NOTIFY][ui] startMultiAgentTaskProbe:already-started');
return;
}
debugNotifyLog('[DEBUG_NOTIFY][ui] startMultiAgentTaskProbe');
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] startMultiAgentTaskProbe', {
conversationId: this.currentConversationId
});
goalModeDebugLog('probe.startMultiAgentTaskProbe', {
conversationId: this.currentConversationId,
taskInProgress: this.taskInProgress
});
this.multiAgentTaskProbeTimer = setInterval(async () => {
if (!this.taskInProgress || !this.currentConversationId) {
debugNotifyLog('[DEBUG_NOTIFY][ui] multiAgentTaskProbe:stop-no-task-in-progress');
goalModeDebugLog('probe.multiAgentTaskProbe_stop', {
reason: 'no_task_in_progress',
taskInProgress: this.taskInProgress,
currentConversationId: this.currentConversationId
});
this.stopMultiAgentTaskProbe();
return;
}
goalModeDebugLog('probe.multiAgentTaskProbe_tick', {
currentConversationId: this.currentConversationId
});
const resumed = await this.tryResumeRunningTask();
if (resumed) {
debugNotifyLog('[DEBUG_NOTIFY][ui] multiAgentTaskProbe:resumed-and-stop');
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] multiAgentTaskProbe:resumed-and-stop');
this.stopMultiAgentTaskProbe();
}
}, 1000);
},
async tryResumeRunningTask() {
if (!this.currentConversationId) {
return false;
}
try {
const { useTaskStore } = await import('../../../stores/task');
const taskStore = useTaskStore();
if (taskStore.hasActiveTask) {
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:already-active', {
taskId: taskStore.currentTaskId,
status: taskStore.taskStatus
});
return true;
}
const resp = await fetch('/api/tasks');
if (!resp.ok) {
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:tasks-api-not-ok', {
status: resp.status
});
return false;
}
const result = await resp.json().catch(() => ({}));
const tasks = Array.isArray(result?.data) ? result.data : [];
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:tasks', {
currentConversationId: this.currentConversationId,
count: tasks.length,
running: tasks
.filter((t: any) => t?.status === 'running')
.map((t: any) => ({
task_id: t?.task_id,
conversation_id: t?.conversation_id,
status: t?.status
}))
});
const runningTask = tasks.find(
(task: any) =>
(task?.status === 'running' || task?.status === 'pending') &&
task?.conversation_id === this.currentConversationId &&
(!(this.versioningHostMode || this.dockerProjectMode) ||
!this.currentHostWorkspaceId ||
task?.workspace_id === this.currentHostWorkspaceId)
);
if (!runningTask?.task_id) {
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:no-matched-task');
goalModeDebugLog('probe.tryResumeRunningTask_no_match', {
currentConversationId: this.currentConversationId,
tasksCount: tasks.length
});
return false;
}
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:resume', {
task_id: runningTask.task_id
});
goalModeDebugLog('probe.tryResumeRunningTask_resume', {
taskId: runningTask.task_id,
conversationId: this.currentConversationId
});
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] tryResumeRunningTask:resume', {
task_id: runningTask.task_id,
conversationId: this.currentConversationId
});
if (typeof this.clearProcessedEvents === 'function') {
this.clearProcessedEvents();
}
taskStore.resumeTask(runningTask.task_id, {
status: 'running',
resetOffset: true,
eventHandler: (event: any) => this.handleTaskEvent(event)
});
return true;
} catch (error) {
console.warn('[TaskPolling] 接管运行中任务失败:', error);
return false;
}
},
async inspectWaitingCompletionState() { async inspectWaitingCompletionState() {
const terminalStatuses = new Set(['completed', 'failed', 'timeout', 'terminated', 'cancelled']); const terminalStatuses = new Set(['completed', 'failed', 'timeout', 'terminated', 'cancelled']);
let hasSubAgentPending = false; let hasSubAgentPending = false;

View File

@ -19,6 +19,7 @@
import { io as createSocketClient } from 'socket.io-client'; import { io as createSocketClient } from 'socket.io-client';
import { renderLatexInRealtime } from './useMarkdownRenderer'; import { renderLatexInRealtime } from './useMarkdownRenderer';
import { getMessageVisibility, messageStartsWork } from '../utils/messageVisibility'; import { getMessageVisibility, messageStartsWork } from '../utils/messageVisibility';
import { goalModeDebugLog } from '../app/methods/common';
export async function initializeLegacySocket(ctx: any) { export async function initializeLegacySocket(ctx: any) {
try { try {
@ -907,10 +908,31 @@ export async function initializeLegacySocket(ctx: any) {
// 用户消息(后台子智能体完成后自动触发) // 用户消息(后台子智能体完成后自动触发)
ctx.socket.on('user_message', (data) => { ctx.socket.on('user_message', (data) => {
if (ctx.usePollingMode && !ctx.waitingForSubAgent) { const isMultiAgentMessage = !!(
data?.multi_agent_message ||
data?.metadata?.multi_agent_message ||
data?.multi_agent_output ||
data?.metadata?.multi_agent_output
);
goalModeDebugLog('legacy_socket_user_message_entry', {
usePollingMode: ctx.usePollingMode,
waitingForSubAgent: ctx.waitingForSubAgent,
isMultiAgentMessage,
currentConversationId: ctx.currentConversationId,
dataConversationId: data?.conversation_id,
message_source: data?.message_source || data?.metadata?.message_source,
starts_work: data?.starts_work,
auto_message_type: data?.auto_message_type || data?.metadata?.auto_message_type,
taskInProgress: ctx.taskInProgress
});
// 多智能体消息在轮询模式下也需要处理,否则主智能体空闲时子智能体
// 推送的消息无法触发前端恢复轮询,必须刷新页面才能看到。
if (ctx.usePollingMode && !ctx.waitingForSubAgent && !isMultiAgentMessage) {
goalModeDebugLog('legacy_socket_user_message_early_return', { reason: 'polling_mode_no_waiting' });
return; return;
} }
if (!data) { if (!data) {
goalModeDebugLog('legacy_socket_user_message_early_return', { reason: 'no_data' });
return; return;
} }
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) { if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
@ -1002,6 +1024,39 @@ export async function initializeLegacySocket(ctx: any) {
} }
})(); })();
} }
// 多智能体消息没有 task_id需要主动查找当前对话的运行中任务并恢复轮询。
if (isMultiAgentMessage && ctx.usePollingMode && !data?.task_id) {
goalModeDebugLog('legacy_socket_user_message_ma_polling_start', {
currentConversationId: ctx.currentConversationId
});
(async () => {
try {
const { useTaskStore } = await import('../stores/task');
const taskStore = useTaskStore();
const runningTask = await taskStore.loadRunningTask(ctx.currentConversationId);
goalModeDebugLog('legacy_socket_user_message_ma_polling_loaded', {
found: !!runningTask,
taskId: runningTask?.task_id,
taskStatus: runningTask?.status
});
if (runningTask?.task_id) {
taskStore.resumeTask(runningTask.task_id);
goalModeDebugLog('legacy_socket_user_message_ma_polling_resumed', {
taskId: runningTask.task_id
});
}
} catch (error) {
console.warn('恢复多智能体任务轮询失败', error);
goalModeDebugLog('legacy_socket_user_message_ma_polling_error', {
error: String(error)
});
}
})();
}
goalModeDebugLog('legacy_socket_user_message_end', {
taskInProgress: ctx.taskInProgress,
streamingMessage: ctx.streamingMessage
});
ctx.$forceUpdate(); ctx.$forceUpdate();
ctx.conditionalScrollToBottom(); ctx.conditionalScrollToBottom();
}); });
@ -1617,9 +1672,13 @@ export async function initializeLegacySocket(ctx: any) {
}); });
socketLog('任务完成', data); socketLog('任务完成', data);
// 多智能体模式下,主任务完成即视为可发送状态,后台子智能体是否 idle/running 不影响输入区 const hasRunningMultiAgent = !!data?.has_running_multi_agent;
if (ctx.multiAgentMode || !data.has_running_sub_agents) { // 多智能体模式下,若仍有运行中的多智能体实例,保持对话运行态,继续接收后续消息;
console.log('[DEBUG] 没有运行中的子智能体,重置任务状态'); // 但此时主智能体已空闲不应阻塞输入区waitingForSubAgent=false
if (hasRunningMultiAgent) {
console.log('[DEBUG] 多智能体实例仍在运行,保持任务状态但不阻塞输入');
} else if (ctx.multiAgentMode || !data.has_running_sub_agents) {
console.log('[DEBUG] 没有运行中的子智能体/多智能体,重置任务状态');
if (ctx.waitingForSubAgent) { if (ctx.waitingForSubAgent) {
ctx.waitingForSubAgent = false; ctx.waitingForSubAgent = false;
} }

View File

@ -411,6 +411,10 @@ export const useTaskStore = defineStore('task', {
startPolling(eventHandler?: (event: any) => void) { startPolling(eventHandler?: (event: any) => void) {
if (this.isPolling) { if (this.isPolling) {
debugLog('[Task] 轮询已在运行'); debugLog('[Task] 轮询已在运行');
goalModeDebugLog('taskStore.startPolling_already_running', {
taskId: this.currentTaskId,
lastEventIndex: this.lastEventIndex
});
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling:already-running', { debugNotifyLog('[DEBUG_NOTIFY][task] startPolling:already-running', {
taskId: this.currentTaskId, taskId: this.currentTaskId,
lastEventIndex: this.lastEventIndex lastEventIndex: this.lastEventIndex
@ -420,10 +424,16 @@ export const useTaskStore = defineStore('task', {
if (!this.currentTaskId) { if (!this.currentTaskId) {
debugLog('[Task] 没有任务ID无法启动轮询'); debugLog('[Task] 没有任务ID无法启动轮询');
goalModeDebugLog('taskStore.startPolling_no_task_id');
return; return;
} }
debugLog('[Task] 启动轮询:', this.currentTaskId); debugLog('[Task] 启动轮询:', this.currentTaskId);
goalModeDebugLog('taskStore.startPolling', {
taskId: this.currentTaskId,
lastEventIndex: this.lastEventIndex,
hasHandler: !!(eventHandler || (window as any).__taskEventHandler)
});
debugNotifyLog('[DEBUG_NOTIFY][task] startPolling', { debugNotifyLog('[DEBUG_NOTIFY][task] startPolling', {
taskId: this.currentTaskId, taskId: this.currentTaskId,
lastEventIndex: this.lastEventIndex lastEventIndex: this.lastEventIndex
@ -470,8 +480,15 @@ export const useTaskStore = defineStore('task', {
} = {} } = {}
) { ) {
if (!taskId) { if (!taskId) {
goalModeDebugLog('taskStore.resumeTask_no_task_id');
return; return;
} }
goalModeDebugLog('taskStore.resumeTask', {
incomingTaskId: taskId,
currentTaskId: this.currentTaskId,
isPolling: this.isPolling,
resetOffset: options?.resetOffset !== false
});
debugNotifyLog('[DEBUG_NOTIFY][task] resumeTask', { debugNotifyLog('[DEBUG_NOTIFY][task] resumeTask', {
incomingTaskId: taskId, incomingTaskId: taskId,
currentTaskId: this.currentTaskId, currentTaskId: this.currentTaskId,
@ -493,6 +510,11 @@ export const useTaskStore = defineStore('task', {
this.pollingError = null; this.pollingError = null;
this.runtimeQueueSnapshotKey = ''; this.runtimeQueueSnapshotKey = '';
this.startPolling(options.eventHandler); this.startPolling(options.eventHandler);
goalModeDebugLog('taskStore.resumeTask_started', {
taskId,
isPolling: this.isPolling,
lastEventIndex: this.lastEventIndex
});
}, },
stopPolling(reason = 'manual') { stopPolling(reason = 'manual') {
@ -547,6 +569,7 @@ export const useTaskStore = defineStore('task', {
async loadRunningTask(conversationId: string | null = null) { async loadRunningTask(conversationId: string | null = null) {
try { try {
debugLog('[Task] 查找运行中的任务'); debugLog('[Task] 查找运行中的任务');
goalModeDebugLog('taskStore.loadRunningTask_start', { conversationId });
const response = await fetch('/api/tasks'); const response = await fetch('/api/tasks');
if (!response.ok) { if (!response.ok) {
@ -568,6 +591,11 @@ export const useTaskStore = defineStore('task', {
if (runningTask) { if (runningTask) {
debugLog('[Task] 发现运行中的任务:', runningTask.task_id); debugLog('[Task] 发现运行中的任务:', runningTask.task_id);
goalModeDebugLog('taskStore.loadRunningTask_found', {
taskId: runningTask.task_id,
status: runningTask.status,
conversationId: runningTask.conversation_id
});
this.currentTaskId = runningTask.task_id; this.currentTaskId = runningTask.task_id;
this.taskStatus = runningTask.status; this.taskStatus = runningTask.status;
@ -603,9 +631,14 @@ export const useTaskStore = defineStore('task', {
} }
debugLog('[Task] 没有运行中的任务'); debugLog('[Task] 没有运行中的任务');
goalModeDebugLog('taskStore.loadRunningTask_not_found', { conversationId });
return null; return null;
} catch (error) { } catch (error) {
console.error('[Task] 加载运行中任务失败:', error); console.error('[Task] 加载运行中任务失败:', error);
goalModeDebugLog('taskStore.loadRunningTask_error', {
conversationId,
error: String(error)
});
return null; return null;
} }
}, },