fix(multi-agent): 修复子智能体状态误判、后台任务隔离与 idle 等待异常
This commit is contained in:
parent
f652118527
commit
7f2ad9144d
@ -16,11 +16,13 @@ def _load_dotenv():
|
|||||||
import sys
|
import sys
|
||||||
pre_existing_keys = set(os.environ.keys())
|
pre_existing_keys = set(os.environ.keys())
|
||||||
|
|
||||||
# 1) 仓库根 .env(开发便利,不覆盖已有环境变量)
|
# 1) 仓库根 .env(开发便利,不覆盖已有的环境变量,但 ASTRION_DATA_ROOT
|
||||||
|
# 作为项目数据根目录必须优先以 .env 为准,避免外部 shell 误指到 clone)
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
env_path = Path.home() / '.astrion' / 'astrion' / '.env'
|
env_path = Path.home() / '.astrion' / 'astrion' / '.env'
|
||||||
else:
|
else:
|
||||||
env_path = Path(__file__).resolve().parents[1] / '.env'
|
env_path = Path(__file__).resolve().parents[1] / '.env'
|
||||||
|
env_from_file: dict = {}
|
||||||
if env_path.exists():
|
if env_path.exists():
|
||||||
try:
|
try:
|
||||||
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
@ -34,11 +36,14 @@ def _load_dotenv():
|
|||||||
value = value.strip().strip('"').strip("'")
|
value = value.strip().strip('"').strip("'")
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
|
env_from_file[key] = value
|
||||||
if key in pre_existing_keys:
|
if key in pre_existing_keys:
|
||||||
continue
|
continue
|
||||||
os.environ[key] = value
|
os.environ[key] = value
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if "ASTRION_DATA_ROOT" in env_from_file:
|
||||||
|
os.environ["ASTRION_DATA_ROOT"] = str(Path(env_from_file["ASTRION_DATA_ROOT"]).expanduser())
|
||||||
|
|
||||||
# 2) settings.json(数据根下的统一配置)
|
# 2) settings.json(数据根下的统一配置)
|
||||||
data_root = os.environ.get("ASTRION_DATA_ROOT", str(Path.home() / ".astrion" / "astrion"))
|
data_root = os.environ.get("ASTRION_DATA_ROOT", str(Path.home() / ".astrion" / "astrion"))
|
||||||
|
|||||||
@ -101,6 +101,7 @@ from utils.api_client import DeepSeekClient
|
|||||||
from utils.context_manager import ContextManager
|
from utils.context_manager import ContextManager
|
||||||
from utils.tool_result_formatter import format_tool_result_for_context
|
from utils.tool_result_formatter import format_tool_result_for_context
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
from config.model_profiles import (
|
from config.model_profiles import (
|
||||||
get_model_profile,
|
get_model_profile,
|
||||||
get_model_prompt_replacements,
|
get_model_prompt_replacements,
|
||||||
@ -1128,6 +1129,27 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
task_ids.append(task.get("task_id"))
|
task_ids.append(task.get("task_id"))
|
||||||
if missing:
|
if missing:
|
||||||
result = {"success": False, "error": f"未找到对应子智能体: {missing}"}
|
result = {"success": False, "error": f"未找到对应子智能体: {missing}"}
|
||||||
|
elif getattr(self, "multi_agent_mode", False):
|
||||||
|
# 多智能体模式:子智能体通过自然输出结束,不调用 finish_task。
|
||||||
|
# sleep 不再阻塞等待,而是直接返回各实例最近一次输出内容。
|
||||||
|
conv_id = self.context_manager.current_conversation_id
|
||||||
|
ma_state = manager.get_multi_agent_state(conv_id)
|
||||||
|
outputs = []
|
||||||
|
for aid in normalized_ids:
|
||||||
|
inst = ma_state.get_instance(aid) if ma_state else None
|
||||||
|
outputs.append({
|
||||||
|
"agent_id": aid,
|
||||||
|
"display_name": inst.display_name if inst else f"Agent_{aid}",
|
||||||
|
"status": inst.status if inst else "unknown",
|
||||||
|
"last_output": inst.last_output if inst else "",
|
||||||
|
})
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"mode": "wait_sub_agent_ids",
|
||||||
|
"agent_ids": normalized_ids,
|
||||||
|
"outputs": outputs,
|
||||||
|
"message": f"已获取 {len(normalized_ids)} 个子智能体最近一次输出"
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
wait_results = []
|
wait_results = []
|
||||||
waited_task_ids = []
|
waited_task_ids = []
|
||||||
@ -1763,7 +1785,7 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
agent_id=int(agent_id),
|
agent_id=int(agent_id),
|
||||||
summary=summary_text,
|
summary=summary_text,
|
||||||
task=arguments.get("task", ""),
|
task=arguments.get("task", ""),
|
||||||
run_in_background=bool(arguments.get("run_in_background", True)),
|
run_in_background=False,
|
||||||
timeout_seconds=arguments.get("timeout_seconds"),
|
timeout_seconds=arguments.get("timeout_seconds"),
|
||||||
conversation_id=conv_id,
|
conversation_id=conv_id,
|
||||||
model_key=role.model_key,
|
model_key=role.model_key,
|
||||||
@ -1774,8 +1796,8 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
task_message=task_message,
|
task_message=task_message,
|
||||||
)
|
)
|
||||||
# 在多智能体模式下,主进程 create_sub_agent 总是后台启动,
|
# 在多智能体模式下,子智能体是团队协作成员,不是传统后台任务。
|
||||||
# 主智能体不需要阻塞等待,而是通过子智能体输出转发拿进度。
|
# run_in_background=False 避免触发后台完成通知轮询,保持主对话输入区可用。
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception("[multi_agent] create_sub_agent failed")
|
logger.exception("[multi_agent] create_sub_agent failed")
|
||||||
result = {"success": False, "error": str(exc)}
|
result = {"success": False, "error": str(exc)}
|
||||||
@ -1865,12 +1887,27 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
else:
|
else:
|
||||||
# 构造消息文本并插入子对话
|
# 构造消息文本并插入子对话
|
||||||
text = build_master_message_to_sub_agent(message)
|
text = build_master_message_to_sub_agent(message)
|
||||||
|
ma_debug(
|
||||||
|
"tool_send_message_to_sub_agent",
|
||||||
|
agent_id=agent_id,
|
||||||
|
raw_message=str(message)[:500],
|
||||||
|
wrapped_message_preview=text[:500],
|
||||||
|
conversation_id=conv_id,
|
||||||
|
)
|
||||||
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
||||||
if not ok:
|
if not ok:
|
||||||
result = {"success": False, "error": f"子智能体 {agent_id} 不存在或已结束"}
|
result = {"success": False, "error": f"子智能体 {agent_id} 不存在或已结束"}
|
||||||
else:
|
else:
|
||||||
result = {"success": True, "agent_id": agent_id}
|
result = {"success": True, "agent_id": agent_id}
|
||||||
|
ma_debug(
|
||||||
|
"tool_send_message_to_sub_agent_result",
|
||||||
|
agent_id=agent_id,
|
||||||
|
conversation_id=conv_id,
|
||||||
|
ok=ok,
|
||||||
|
result=result,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
logger.exception("[multi_agent] send_message_to_sub_agent failed")
|
||||||
result = {"success": False, "error": str(exc)}
|
result = {"success": False, "error": str(exc)}
|
||||||
|
|
||||||
elif tool_name == "ask_sub_agent":
|
elif tool_name == "ask_sub_agent":
|
||||||
@ -1896,6 +1933,14 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
msg_id=question_id,
|
msg_id=question_id,
|
||||||
target=state.get_instance(agent_id).display_name if state.get_instance(agent_id) else f"Agent_{agent_id}",
|
target=state.get_instance(agent_id).display_name if state.get_instance(agent_id) else f"Agent_{agent_id}",
|
||||||
)
|
)
|
||||||
|
ma_debug(
|
||||||
|
"tool_ask_sub_agent",
|
||||||
|
agent_id=agent_id,
|
||||||
|
question=str(question)[:500],
|
||||||
|
question_id=question_id,
|
||||||
|
wrapped_message_preview=text[:500],
|
||||||
|
conversation_id=conv_id,
|
||||||
|
)
|
||||||
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
||||||
if not ok:
|
if not ok:
|
||||||
result = {"success": False, "error": f"子智能体 {agent_id} 不存在"}
|
result = {"success": False, "error": f"子智能体 {agent_id} 不存在"}
|
||||||
@ -1921,6 +1966,12 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
answer = arguments.get("answer", "")
|
answer = arguments.get("answer", "")
|
||||||
conv_id = self.context_manager.current_conversation_id
|
conv_id = self.context_manager.current_conversation_id
|
||||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||||
|
ma_debug(
|
||||||
|
"tool_answer_sub_agent_question",
|
||||||
|
question_id=question_id,
|
||||||
|
answer_preview=str(answer)[:500],
|
||||||
|
conversation_id=conv_id,
|
||||||
|
)
|
||||||
if not state:
|
if not state:
|
||||||
result = {"success": False, "error": "多智能体状态未就绪"}
|
result = {"success": False, "error": "多智能体状态未就绪"}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Callable, TYPE_CHECKING
|
from typing import Any, Dict, List, Optional, Callable, TYPE_CHECKING
|
||||||
import os
|
import os
|
||||||
from core.main_terminal import MainTerminal
|
from core.main_terminal import MainTerminal
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
@ -105,13 +105,19 @@ class WebTerminal(MainTerminal):
|
|||||||
# 新增:对话管理相关方法(Web版本)
|
# 新增:对话管理相关方法(Web版本)
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|
||||||
def create_new_conversation(self, thinking_mode: bool = None, run_mode: Optional[str] = None) -> Dict:
|
def create_new_conversation(
|
||||||
|
self,
|
||||||
|
thinking_mode: bool = None,
|
||||||
|
run_mode: Optional[str] = None,
|
||||||
|
metadata_overrides: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
创建新对话(Web版本)
|
创建新对话(Web版本)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
thinking_mode: 思考模式,None则使用当前设置
|
thinking_mode: 思考模式,None则使用当前设置
|
||||||
run_mode: 显式的运行模式(fast/thinking/deep)
|
run_mode: 显式的运行模式(fast/thinking/deep)
|
||||||
|
metadata_overrides: 额外写入对话 metadata 的字段
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 包含新对话信息
|
Dict: 包含新对话信息
|
||||||
@ -171,17 +177,21 @@ class WebTerminal(MainTerminal):
|
|||||||
try:
|
try:
|
||||||
# 先创建新对话。start_new_conversation 会先 save_current_conversation(),
|
# 先创建新对话。start_new_conversation 会先 save_current_conversation(),
|
||||||
# 此时 terminal.model_key 仍是旧对话的模型,避免把旧对话覆盖成默认模型。
|
# 此时 terminal.model_key 仍是旧对话的模型,避免把旧对话覆盖成默认模型。
|
||||||
|
metadata_overrides_merged = {
|
||||||
|
"permission_mode": self.get_permission_mode(),
|
||||||
|
"execution_mode": self.get_execution_mode() if hasattr(self, "get_execution_mode") else "sandbox",
|
||||||
|
"pending_permission_mode": None,
|
||||||
|
"pending_execution_mode": None,
|
||||||
|
# frozen_*_prompt 不在创建时预设,由第一次 build_messages 根据当时的实际模式懒加载并冻结
|
||||||
|
}
|
||||||
|
if isinstance(metadata_overrides, dict):
|
||||||
|
metadata_overrides_merged.update(metadata_overrides)
|
||||||
|
|
||||||
conversation_id = self.context_manager.start_new_conversation(
|
conversation_id = self.context_manager.start_new_conversation(
|
||||||
project_path=self.project_path,
|
project_path=self.project_path,
|
||||||
thinking_mode=thinking_mode,
|
thinking_mode=thinking_mode,
|
||||||
run_mode=self.run_mode,
|
run_mode=self.run_mode,
|
||||||
metadata_overrides={
|
metadata_overrides=metadata_overrides_merged,
|
||||||
"permission_mode": self.get_permission_mode(),
|
|
||||||
"execution_mode": self.get_execution_mode() if hasattr(self, "get_execution_mode") else "sandbox",
|
|
||||||
"pending_permission_mode": None,
|
|
||||||
"pending_execution_mode": None,
|
|
||||||
# frozen_*_prompt 不在创建时预设,由第一次 build_messages 根据当时的实际模式懒加载并冻结
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 新对话创建完成后再应用默认模型(此时旧对话已安全保存)。
|
# 新对话创建完成后再应用默认模型(此时旧对话已安全保存)。
|
||||||
@ -377,6 +387,7 @@ class WebTerminal(MainTerminal):
|
|||||||
"run_mode": self.run_mode,
|
"run_mode": self.run_mode,
|
||||||
"thinking_mode": self.thinking_mode,
|
"thinking_mode": self.thinking_mode,
|
||||||
"model_key": getattr(self, "model_key", None),
|
"model_key": getattr(self, "model_key", None),
|
||||||
|
"multi_agent_mode": bool(getattr(self, "multi_agent_mode", False)),
|
||||||
"message": f"对话已加载: {conversation_id}"
|
"message": f"对话已加载: {conversation_id}"
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
|||||||
30
modules/multi_agent/debug_logger.py
Normal file
30
modules/multi_agent/debug_logger.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""多智能体模式专用调试日志。
|
||||||
|
|
||||||
|
把所有子智能体与主智能体之间的消息往来、注入、转发、工具调用记录下来,
|
||||||
|
便于排查循环/重复输出等问题。日志写入 {LOGS_DIR}/multi_agent_loop.log。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from config.paths import LOGS_DIR
|
||||||
|
|
||||||
|
|
||||||
|
_LOG_PATH = Path(LOGS_DIR) / "multi_agent_loop.log"
|
||||||
|
|
||||||
|
|
||||||
|
def ma_debug(event: str, **kwargs: Any) -> None:
|
||||||
|
"""追加一条结构化调试日志。"""
|
||||||
|
try:
|
||||||
|
_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {"t": time.time(), "event": event}
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
payload[k] = v
|
||||||
|
line = json.dumps(payload, ensure_ascii=False, default=str)
|
||||||
|
with open(_LOG_PATH, "a", encoding="utf-8") as f:
|
||||||
|
f.write(line + "\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@ -25,6 +25,7 @@ from dataclasses import dataclass, field
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||||
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from modules.sub_agent.task import SubAgentTask
|
from modules.sub_agent.task import SubAgentTask
|
||||||
@ -298,7 +299,17 @@ class MultiAgentState:
|
|||||||
if question_id in self.pending_answers:
|
if question_id in self.pending_answers:
|
||||||
return self.pending_answers.pop(question_id)
|
return self.pending_answers.pop(question_id)
|
||||||
if question_id in self.pending_questions:
|
if question_id in self.pending_questions:
|
||||||
raise RuntimeError(f"question_id 已存在: {question_id}")
|
old_fut = self.pending_questions[question_id]
|
||||||
|
try:
|
||||||
|
old_loop = old_fut.get_loop()
|
||||||
|
if old_loop.is_closed():
|
||||||
|
self.pending_questions.pop(question_id, None)
|
||||||
|
self.pending_question_loops.pop(question_id, None)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"question_id 已存在: {question_id}")
|
||||||
|
except Exception:
|
||||||
|
self.pending_questions.pop(question_id, None)
|
||||||
|
self.pending_question_loops.pop(question_id, None)
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
fut: asyncio.Future = loop.create_future()
|
fut: asyncio.Future = loop.create_future()
|
||||||
self.pending_questions[question_id] = fut
|
self.pending_questions[question_id] = fut
|
||||||
@ -329,6 +340,12 @@ class MultiAgentState:
|
|||||||
返回 True 表示找到等待中的 future;False 表示无等待方或已超时。
|
返回 True 表示找到等待中的 future;False 表示无等待方或已超时。
|
||||||
支持跨事件循环调用(例如主对话循环回答子智能体循环里的提问)。
|
支持跨事件循环调用(例如主对话循环回答子智能体循环里的提问)。
|
||||||
"""
|
"""
|
||||||
|
ma_debug(
|
||||||
|
"state_provide_answer",
|
||||||
|
question_id=question_id,
|
||||||
|
has_pending=question_id in self.pending_questions,
|
||||||
|
answer_preview=str(answer)[:300],
|
||||||
|
)
|
||||||
# 如果 wait_for_answer 还没注册,先把答案暂存
|
# 如果 wait_for_answer 还没注册,先把答案暂存
|
||||||
if question_id not in self.pending_questions:
|
if question_id not in self.pending_questions:
|
||||||
self.pending_answers[question_id] = answer
|
self.pending_answers[question_id] = answer
|
||||||
@ -349,8 +366,14 @@ class MultiAgentState:
|
|||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# 同循环回退
|
# 同循环回退(future 所属循环可能已关闭,失败时把答案暂存,避免阻塞方永远等不到)
|
||||||
return asyncio.run_coroutine_threadsafe(self._do_provide_answer(question_id, answer), asyncio.get_event_loop()).result(timeout=5)
|
try:
|
||||||
|
return asyncio.run_coroutine_threadsafe(self._do_provide_answer(question_id, answer), asyncio.get_event_loop()).result(timeout=5)
|
||||||
|
except Exception:
|
||||||
|
self.pending_questions.pop(question_id, None)
|
||||||
|
self.pending_question_loops.pop(question_id, None)
|
||||||
|
self.pending_answers[question_id] = answer
|
||||||
|
return True
|
||||||
|
|
||||||
def is_agent_blocking(self, agent_id: int) -> bool:
|
def is_agent_blocking(self, agent_id: int) -> bool:
|
||||||
return agent_id in self.agent_blocking_question
|
return agent_id in self.agent_blocking_question
|
||||||
|
|||||||
@ -53,7 +53,10 @@ def _master_tool_create_sub_agent() -> Dict[str, Any]:
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "(可选)手动指定实例编号;不传时自动递增。",
|
"description": "(可选)手动指定实例编号;不传时自动递增。",
|
||||||
},
|
},
|
||||||
"timeout_seconds": {"type": "integer", "description": "超时秒数,默认 600。"},
|
"timeout_seconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "(可选)超时秒数。不填表示该子智能体不会被时间终结,适用于可能多轮长期任务的子智能体;一般情况下推荐创建永久子智能体。",
|
||||||
|
},
|
||||||
"thinking_mode": {
|
"thinking_mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["fast", "thinking"],
|
"enum": ["fast", "thinking"],
|
||||||
|
|||||||
@ -30,6 +30,7 @@ from modules.sub_agent.tools import handle_search_workspace, handle_read_mediafi
|
|||||||
from modules.sub_agent.state import SubAgentStateMixin
|
from modules.sub_agent.state import SubAgentStateMixin
|
||||||
from modules.sub_agent.stats import SubAgentStatsMixin
|
from modules.sub_agent.stats import SubAgentStatsMixin
|
||||||
from modules.sub_agent.creation import SubAgentCreationMixin
|
from modules.sub_agent.creation import SubAgentCreationMixin
|
||||||
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.web_terminal import WebTerminal
|
from core.web_terminal import WebTerminal
|
||||||
@ -199,7 +200,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
if task_message:
|
if task_message:
|
||||||
user_message = task_message
|
user_message = task_message
|
||||||
else:
|
else:
|
||||||
user_message = build_user_message(agent_id, summary, task, deliverables_display, timeout_seconds or SUB_AGENT_DEFAULT_TIMEOUT)
|
display_timeout = timeout_seconds if timeout_seconds is not None else 0
|
||||||
|
user_message = build_user_message(agent_id, summary, task, deliverables_display, display_timeout or SUB_AGENT_DEFAULT_TIMEOUT)
|
||||||
task_file.write_text(user_message, encoding="utf-8")
|
task_file.write_text(user_message, encoding="utf-8")
|
||||||
|
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
@ -208,8 +210,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
final_system_prompt = build_system_prompt(prompt_workspace)
|
final_system_prompt = build_system_prompt(prompt_workspace)
|
||||||
system_prompt_file.write_text(final_system_prompt, encoding="utf-8")
|
system_prompt_file.write_text(final_system_prompt, encoding="utf-8")
|
||||||
|
|
||||||
timeout_seconds = timeout_seconds or SUB_AGENT_DEFAULT_TIMEOUT
|
# timeout_seconds 为 None 表示永久子智能体(不会被时间终结)
|
||||||
|
|
||||||
task_record = {
|
task_record = {
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"agent_id": agent_id,
|
"agent_id": agent_id,
|
||||||
@ -223,6 +224,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
"updated_at": time.time(),
|
"updated_at": time.time(),
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"run_in_background": run_in_background,
|
"run_in_background": run_in_background,
|
||||||
|
"multi_agent_mode": bool(multi_agent_mode),
|
||||||
"task_root": str(task_root),
|
"task_root": str(task_root),
|
||||||
"output_file": str(output_file),
|
"output_file": str(output_file),
|
||||||
"stats_file": str(stats_file),
|
"stats_file": str(stats_file),
|
||||||
@ -286,6 +288,15 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
if multi_agent_mode and display_name:
|
if multi_agent_mode and display_name:
|
||||||
message = f"{display_name} 已创建,任务ID: {task_id}"
|
message = f"{display_name} 已创建,任务ID: {task_id}"
|
||||||
print(f"{OUTPUT_FORMATS['info']} {message}")
|
print(f"{OUTPUT_FORMATS['info']} {message}")
|
||||||
|
ma_debug(
|
||||||
|
"manager_create_sub_agent",
|
||||||
|
task_id=task_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
display_name=display_name,
|
||||||
|
multi_agent_mode=multi_agent_mode,
|
||||||
|
run_in_background=task_record.get("run_in_background"),
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@ -577,6 +588,26 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
"""删除会话状态(会话结束时调用)。"""
|
"""删除会话状态(会话结束时调用)。"""
|
||||||
self.multi_agent_states.pop(conversation_id, None)
|
self.multi_agent_states.pop(conversation_id, None)
|
||||||
|
|
||||||
|
def reconcile_task_states(self, conversation_id: Optional[str] = None) -> int:
|
||||||
|
"""修正运行态任务状态。
|
||||||
|
|
||||||
|
在父类实现前先根据内存中的 MultiAgentState 给旧任务补上 multi_agent_mode
|
||||||
|
标记,避免任务记录缺字段导致被当成普通子智能体误判为 failed。
|
||||||
|
"""
|
||||||
|
if conversation_id and conversation_id in self.multi_agent_states:
|
||||||
|
state = self.multi_agent_states[conversation_id]
|
||||||
|
agent_ids = {a.agent_id for a in state.list_all()}
|
||||||
|
for task in self.tasks.values():
|
||||||
|
if (
|
||||||
|
isinstance(task, dict)
|
||||||
|
and task.get("conversation_id") == conversation_id
|
||||||
|
and task.get("agent_id") in agent_ids
|
||||||
|
and task.get("multi_agent_mode") is None
|
||||||
|
):
|
||||||
|
task["multi_agent_mode"] = True
|
||||||
|
task["updated_at"] = time.time()
|
||||||
|
return super().reconcile_task_states(conversation_id=conversation_id)
|
||||||
|
|
||||||
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 消息。
|
||||||
|
|
||||||
@ -585,6 +616,13 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
"""
|
"""
|
||||||
# 查找该 agent_id 对应的 running SubAgentTask
|
# 查找该 agent_id 对应的 running SubAgentTask
|
||||||
sub_agent = self._find_sub_agent_task_by_agent_id(agent_id)
|
sub_agent = self._find_sub_agent_task_by_agent_id(agent_id)
|
||||||
|
ma_debug(
|
||||||
|
"manager_inject_message_to_sub_agent",
|
||||||
|
agent_id=agent_id,
|
||||||
|
message_preview=str(message_text)[:500],
|
||||||
|
found=bool(sub_agent),
|
||||||
|
task_id=sub_agent.task_id if sub_agent else None,
|
||||||
|
)
|
||||||
if not sub_agent:
|
if not sub_agent:
|
||||||
return False
|
return False
|
||||||
sub_agent.inject_message(message_text)
|
sub_agent.inject_message(message_text)
|
||||||
@ -606,18 +644,40 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
|
|
||||||
def _on_multi_agent_task_done(self, task_id: str, agent_id: int, state: Any, sub_agent: Any) -> None:
|
def _on_multi_agent_task_done(self, task_id: str, agent_id: int, state: Any, sub_agent: Any) -> None:
|
||||||
"""SubAgentTask 结束回调会调这个更新 MultiAgentState 实例状态。"""
|
"""SubAgentTask 结束回调会调这个更新 MultiAgentState 实例状态。"""
|
||||||
# 取出当前 status(由 _finalize_task 设置)
|
|
||||||
final_task = self.tasks.get(task_id) or {}
|
final_task = self.tasks.get(task_id) or {}
|
||||||
final_status = final_task.get("status") or "terminated"
|
final_status_before = final_task.get("status")
|
||||||
# 允许的自然退出:running -> idle(未走 _finalize) vs failed/timeout
|
ma_debug(
|
||||||
# SubAgentTask 在多智能体模式下没有 _finalize_task,我们手动赋值 idle
|
"manager_on_multi_agent_task_done",
|
||||||
# 如果状态被结 _finalize 为 failed/timeout,则保持该状态
|
task_id=task_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
sub_agent_idle=getattr(sub_agent, "_idle", False),
|
||||||
|
sub_agent_cancelled=getattr(sub_agent, "_cancelled", False),
|
||||||
|
task_status_before=final_status_before,
|
||||||
|
)
|
||||||
|
# 多智能体模式下,子智能体自然进入 idle 后 Task 可能被外部事件循环取消,
|
||||||
|
# 或者 reconcile 把 idle 误判为 failed。优先以 SubAgentTask 自身状态为准:
|
||||||
|
# - 被手动取消 -> terminated
|
||||||
|
# - 自然进入 idle -> idle(可继续接收消息)
|
||||||
|
# - 真正异常/超时/finish_task 失败 -> failed/timeout
|
||||||
|
if getattr(sub_agent, "_cancelled", False):
|
||||||
|
state.mark_status(agent_id, "terminated")
|
||||||
|
ma_debug("manager_ma_state_set", agent_id=agent_id, status="terminated", reason="sub_agent_cancelled")
|
||||||
|
return
|
||||||
|
if getattr(sub_agent, "_idle", False):
|
||||||
|
state.mark_status(agent_id, "idle")
|
||||||
|
ma_debug("manager_ma_state_set", agent_id=agent_id, status="idle", reason="sub_agent_idle")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 兜底:取出当前 task status(由 _finalize_task 设置)
|
||||||
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")
|
||||||
elif final_status == "terminated":
|
elif final_status == "terminated":
|
||||||
state.mark_status(agent_id, "terminated")
|
state.mark_status(agent_id, "terminated")
|
||||||
|
ma_debug("manager_ma_state_set", agent_id=agent_id, status="terminated", reason="task_terminated_status")
|
||||||
else:
|
else:
|
||||||
state.mark_status(agent_id, "idle")
|
state.mark_status(agent_id, "idle")
|
||||||
|
ma_debug("manager_ma_state_set", agent_id=agent_id, status="idle", reason="fallback_idle")
|
||||||
|
|
||||||
def _get_runtime_path(self, host_path: Path) -> str:
|
def _get_runtime_path(self, host_path: Path) -> str:
|
||||||
"""将宿主机路径映射为容器内路径(仅用于提示展示)。"""
|
"""将宿主机路径映射为容器内路径(仅用于提示展示)。"""
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
TERMINAL_STATUSES = {"completed", "failed", "timeout"}
|
TERMINAL_STATUSES = {"completed", "failed", "timeout"}
|
||||||
@ -94,6 +95,7 @@ class SubAgentStateMixin:
|
|||||||
try:
|
try:
|
||||||
output = json.loads(output_file.read_text(encoding="utf-8"))
|
output = json.loads(output_file.read_text(encoding="utf-8"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
logger.warning(f"[check_task_status] output 文件解析失败: {exc}")
|
||||||
task["status"] = "failed"
|
task["status"] = "failed"
|
||||||
task["updated_at"] = time.time()
|
task["updated_at"] = time.time()
|
||||||
return {"success": False, "status": "failed", "task_id": task_id, "message": f"输出文件解析失败: {exc}"}
|
return {"success": False, "status": "failed", "task_id": task_id, "message": f"输出文件解析失败: {exc}"}
|
||||||
@ -103,6 +105,24 @@ class SubAgentStateMixin:
|
|||||||
stats = output.get("stats", {})
|
stats = output.get("stats", {})
|
||||||
elapsed_seconds = self._compute_elapsed_seconds(task)
|
elapsed_seconds = self._compute_elapsed_seconds(task)
|
||||||
|
|
||||||
|
ma_debug(
|
||||||
|
"check_task_status",
|
||||||
|
task_id=task_id,
|
||||||
|
agent_id=task.get("agent_id"),
|
||||||
|
multi_agent_mode=task.get("multi_agent_mode"),
|
||||||
|
output_status=output.get("status"),
|
||||||
|
output_success=success,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 多智能体模式:output 中 status 为 running/idle 表示子智能体仍在运行或
|
||||||
|
# 本轮结束但上下文保留、可继续接收消息,都不是失败/完成。
|
||||||
|
# 此时不生成 final_result,只保持原状态,避免 reconcile 把运行中误判为失败。
|
||||||
|
if task.get("multi_agent_mode") and output.get("status") in {"running", "idle"}:
|
||||||
|
task["status"] = output["status"]
|
||||||
|
task["updated_at"] = time.time()
|
||||||
|
ma_debug("check_task_status_keep_alive", task_id=task_id, status=output["status"])
|
||||||
|
return {"status": output["status"], "task_id": task_id}
|
||||||
|
|
||||||
if output.get("timeout"):
|
if output.get("timeout"):
|
||||||
status = "timeout"
|
status = "timeout"
|
||||||
elif output.get("max_turns_exceeded"):
|
elif output.get("max_turns_exceeded"):
|
||||||
@ -113,6 +133,7 @@ class SubAgentStateMixin:
|
|||||||
else:
|
else:
|
||||||
status = "failed"
|
status = "failed"
|
||||||
|
|
||||||
|
ma_debug("check_task_status_result", task_id=task_id, agent_id=task.get("agent_id"), status=status)
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
task["updated_at"] = time.time()
|
task["updated_at"] = time.time()
|
||||||
if status == "completed" and elapsed_seconds is not None:
|
if status == "completed" and elapsed_seconds is not None:
|
||||||
@ -237,6 +258,13 @@ class SubAgentStateMixin:
|
|||||||
if not task:
|
if not task:
|
||||||
return
|
return
|
||||||
status = "completed" if success else "failed"
|
status = "completed" if success else "failed"
|
||||||
|
ma_debug(
|
||||||
|
"mark_task_done",
|
||||||
|
task_id=task_id,
|
||||||
|
agent_id=task.get("agent_id"),
|
||||||
|
success=success,
|
||||||
|
new_status=status,
|
||||||
|
)
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
task["updated_at"] = time.time()
|
task["updated_at"] = time.time()
|
||||||
task["runtime_seconds"] = runtime_seconds
|
task["runtime_seconds"] = runtime_seconds
|
||||||
@ -246,10 +274,19 @@ class SubAgentStateMixin:
|
|||||||
def _refresh_task_runtime_state(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
def _refresh_task_runtime_state(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""刷新单个任务运行态。"""
|
"""刷新单个任务运行态。"""
|
||||||
status = task.get("status")
|
status = task.get("status")
|
||||||
if status in TERMINAL_STATUSES.union({"terminated"}):
|
|
||||||
return {"status": status, "task_id": task.get("task_id")}
|
|
||||||
|
|
||||||
task_id = task.get("task_id")
|
task_id = task.get("task_id")
|
||||||
|
agent_id = task.get("agent_id")
|
||||||
|
multi_agent_flag = task.get("multi_agent_mode")
|
||||||
|
ma_debug(
|
||||||
|
"refresh_task_runtime_state_start",
|
||||||
|
task_id=task_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
before_status=status,
|
||||||
|
multi_agent_mode=multi_agent_flag,
|
||||||
|
)
|
||||||
|
if status in TERMINAL_STATUSES.union({"terminated"}):
|
||||||
|
return {"status": status, "task_id": task_id}
|
||||||
|
|
||||||
running_task = self._running_tasks.get(task_id) if task_id else None
|
running_task = self._running_tasks.get(task_id) if task_id else None
|
||||||
if running_task:
|
if running_task:
|
||||||
if running_task.done():
|
if running_task.done():
|
||||||
@ -261,18 +298,25 @@ class SubAgentStateMixin:
|
|||||||
if task.get("multi_agent_mode") and not Path(task.get("output_file", "")).exists():
|
if task.get("multi_agent_mode") and not Path(task.get("output_file", "")).exists():
|
||||||
task["status"] = "idle"
|
task["status"] = "idle"
|
||||||
task["updated_at"] = time.time()
|
task["updated_at"] = time.time()
|
||||||
|
ma_debug("refresh_task_runtime_state_idle_no_output", task_id=task_id)
|
||||||
return {"status": "idle", "task_id": task_id}
|
return {"status": "idle", "task_id": task_id}
|
||||||
return self._check_task_status(task)
|
result = self._check_task_status(task)
|
||||||
|
ma_debug("refresh_task_runtime_state_done_result", task_id=task_id, result=result)
|
||||||
|
return result
|
||||||
|
ma_debug("refresh_task_runtime_state_still_running", task_id=task_id)
|
||||||
return {"status": "running", "task_id": task_id}
|
return {"status": "running", "task_id": task_id}
|
||||||
|
|
||||||
output_file = Path(task.get("output_file", ""))
|
output_file = Path(task.get("output_file", ""))
|
||||||
if output_file.exists():
|
if output_file.exists():
|
||||||
return self._check_task_status(task)
|
result = self._check_task_status(task)
|
||||||
|
ma_debug("refresh_task_runtime_state_output_result", task_id=task_id, result=result)
|
||||||
|
return result
|
||||||
|
|
||||||
# 多智能体模式:没有输出文件表示 idle,不强制清理
|
# 多智能体模式:没有输出文件表示 idle,不强制清理
|
||||||
if task.get("multi_agent_mode"):
|
if task.get("multi_agent_mode"):
|
||||||
task["status"] = "idle"
|
task["status"] = "idle"
|
||||||
task["updated_at"] = time.time()
|
task["updated_at"] = time.time()
|
||||||
|
ma_debug("refresh_task_runtime_state_idle_no_output_file", task_id=task_id)
|
||||||
return {"status": "idle", "task_id": task_id}
|
return {"status": "idle", "task_id": task_id}
|
||||||
|
|
||||||
if self._should_force_cleanup_stale_task(task):
|
if self._should_force_cleanup_stale_task(task):
|
||||||
@ -293,6 +337,7 @@ class SubAgentStateMixin:
|
|||||||
def reconcile_task_states(self, conversation_id: Optional[str] = None) -> int:
|
def reconcile_task_states(self, conversation_id: Optional[str] = None) -> int:
|
||||||
"""修正运行态任务状态,返回修正条目数。"""
|
"""修正运行态任务状态,返回修正条目数。"""
|
||||||
changed = 0
|
changed = 0
|
||||||
|
ma_debug("reconcile_task_states_start", conversation_id=conversation_id, task_count=len(self.tasks))
|
||||||
for task in self.tasks.values():
|
for task in self.tasks.values():
|
||||||
if not isinstance(task, dict):
|
if not isinstance(task, dict):
|
||||||
continue
|
continue
|
||||||
@ -301,10 +346,19 @@ class SubAgentStateMixin:
|
|||||||
before_status = task.get("status")
|
before_status = task.get("status")
|
||||||
before_notified = task.get("notified")
|
before_notified = task.get("notified")
|
||||||
self._refresh_task_runtime_state(task)
|
self._refresh_task_runtime_state(task)
|
||||||
if task.get("status") != before_status or task.get("notified") != before_notified:
|
after_status = task.get("status")
|
||||||
|
if after_status != before_status or task.get("notified") != before_notified:
|
||||||
|
ma_debug(
|
||||||
|
"reconcile_task_states_changed",
|
||||||
|
task_id=task.get("task_id"),
|
||||||
|
agent_id=task.get("agent_id"),
|
||||||
|
before_status=before_status,
|
||||||
|
after_status=after_status,
|
||||||
|
)
|
||||||
changed += 1
|
changed += 1
|
||||||
if changed:
|
if changed:
|
||||||
self._save_state()
|
self._save_state()
|
||||||
|
ma_debug("reconcile_task_states_end", conversation_id=conversation_id, changed=changed)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def _should_force_cleanup_stale_task(self, task: Dict[str, Any]) -> bool:
|
def _should_force_cleanup_stale_task(self, task: Dict[str, Any]) -> bool:
|
||||||
@ -314,8 +368,12 @@ class SubAgentStateMixin:
|
|||||||
created_at = 0
|
created_at = 0
|
||||||
if created_at <= 0:
|
if created_at <= 0:
|
||||||
return False
|
return False
|
||||||
|
# timeout_seconds 为 None 表示永久子智能体,不强制清理
|
||||||
|
raw_timeout = task.get("timeout_seconds")
|
||||||
|
if raw_timeout is None:
|
||||||
|
return False
|
||||||
from config import SUB_AGENT_DEFAULT_TIMEOUT
|
from config import SUB_AGENT_DEFAULT_TIMEOUT
|
||||||
timeout_seconds = int(task.get("timeout_seconds") or SUB_AGENT_DEFAULT_TIMEOUT or 0)
|
timeout_seconds = int(raw_timeout or SUB_AGENT_DEFAULT_TIMEOUT or 0)
|
||||||
timeout_seconds = max(timeout_seconds, 1)
|
timeout_seconds = max(timeout_seconds, 1)
|
||||||
grace_seconds = 120
|
grace_seconds = 120
|
||||||
elapsed = time.time() - created_at
|
elapsed = time.time() - created_at
|
||||||
|
|||||||
@ -27,6 +27,8 @@ if TYPE_CHECKING:
|
|||||||
from modules.sub_agent.manager import SubAgentManager
|
from modules.sub_agent.manager import SubAgentManager
|
||||||
from modules.multi_agent.state import MultiAgentState
|
from modules.multi_agent.state import MultiAgentState
|
||||||
|
|
||||||
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
# 多智能体模式下额外加载的工具定义
|
# 多智能体模式下额外加载的工具定义
|
||||||
@ -63,7 +65,8 @@ class SubAgentTask:
|
|||||||
self.thinking_mode = thinking_mode or "fast"
|
self.thinking_mode = thinking_mode or "fast"
|
||||||
self.task_id = task_record["task_id"]
|
self.task_id = task_record["task_id"]
|
||||||
self.agent_id = task_record["agent_id"]
|
self.agent_id = task_record["agent_id"]
|
||||||
self.timeout_seconds = int(task_record.get("timeout_seconds") or 180)
|
raw_timeout = task_record.get("timeout_seconds")
|
||||||
|
self.timeout_seconds = int(raw_timeout) if raw_timeout is not None else None
|
||||||
self.deliverables_dir = Path(task_record["deliverables_dir"])
|
self.deliverables_dir = Path(task_record["deliverables_dir"])
|
||||||
|
|
||||||
self.output_file = Path(task_record["output_file"])
|
self.output_file = Path(task_record["output_file"])
|
||||||
@ -97,8 +100,9 @@ class SubAgentTask:
|
|||||||
# display_name 不传时回退为 'Agent_{self.agent_id}'
|
# display_name 不传时回退为 'Agent_{self.agent_id}'
|
||||||
self.display_name = display_name or f"Agent_{self.agent_id}"
|
self.display_name = display_name or f"Agent_{self.agent_id}"
|
||||||
# 多智能体运行期控制
|
# 多智能体运行期控制
|
||||||
# 使用 threading.Event 避免跨事件循环唤醒问题
|
# 使用 asyncio.Event 在子智能体自己的事件循环内等待;
|
||||||
self._continue_event = threading.Event()
|
# inject_message 可能跨线程调用,通过 loop.call_soon_threadsafe 唤醒。
|
||||||
|
self._continue_event: Optional[asyncio.Event] = None
|
||||||
self._idle = False
|
self._idle = False
|
||||||
self._pending_answer_question_id: Optional[str] = None
|
self._pending_answer_question_id: Optional[str] = None
|
||||||
self._answered_question_ids: Set[str] = set()
|
self._answered_question_ids: Set[str] = set()
|
||||||
@ -116,16 +120,20 @@ class SubAgentTask:
|
|||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""主 LLM 循环。"""
|
"""主 LLM 循环。"""
|
||||||
|
# 在子智能体自己的事件循环内初始化 asyncio.Event
|
||||||
|
self._continue_event = asyncio.Event()
|
||||||
try:
|
try:
|
||||||
await self._run_loop()
|
await self._run_loop()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self._cancelled = True
|
self._cancelled = True
|
||||||
logger.debug(f"[SubAgent] task={self.task_id} 被取消")
|
logger.debug(f"[SubAgent] task={self.task_id} 被取消")
|
||||||
|
ma_debug("sub_agent_run_cancelled", task_id=self.task_id, agent_id=self.agent_id, display_name=self.display_name)
|
||||||
# shield 避免取消信号中断最终状态落盘
|
# shield 避免取消信号中断最终状态落盘
|
||||||
await asyncio.shield(self._write_failure("子智能体被手动终止"))
|
await asyncio.shield(self._write_failure("子智能体被手动终止"))
|
||||||
raise
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(f"[SubAgent] task={self.task_id} 执行异常")
|
logger.exception(f"[SubAgent] task={self.task_id} 执行异常")
|
||||||
|
ma_debug("sub_agent_run_exception", task_id=self.task_id, agent_id=self.agent_id, display_name=self.display_name, error=str(exc))
|
||||||
await self._write_failure(f"执行异常: {exc}")
|
await self._write_failure(f"执行异常: {exc}")
|
||||||
|
|
||||||
async def _run_loop(self) -> None:
|
async def _run_loop(self) -> None:
|
||||||
@ -144,22 +152,33 @@ class SubAgentTask:
|
|||||||
|
|
||||||
while not self._cancelled:
|
while not self._cancelled:
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
if elapsed > self.timeout_seconds:
|
if self.timeout_seconds is not None and elapsed > self.timeout_seconds:
|
||||||
await self._write_timeout(elapsed)
|
await self._write_timeout(elapsed)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 多智能体模式下,idle 时等待新消息或外部回答;超时后继续循环检查
|
# 多智能体模式下,idle 时等待新消息或外部回答;只有真正被注入消息时才继续运行
|
||||||
if self.multi_agent_mode and self._idle:
|
if self.multi_agent_mode and self._idle:
|
||||||
|
event_set = False
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(
|
if self._continue_event is None:
|
||||||
asyncio.get_event_loop().run_in_executor(None, self._continue_event.wait),
|
self._continue_event = asyncio.Event()
|
||||||
timeout=1.0,
|
await asyncio.wait_for(self._continue_event.wait(), timeout=1.0)
|
||||||
)
|
event_set = True
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
pass
|
pass
|
||||||
self._continue_event.clear()
|
|
||||||
if self._cancelled:
|
if self._cancelled:
|
||||||
break
|
break
|
||||||
|
if not event_set:
|
||||||
|
# 只是周期性检查取消状态,没有新消息,保持 idle 继续等待
|
||||||
|
continue
|
||||||
|
self._continue_event.clear()
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_idle_wake",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
pending_messages=[m.get("role") for m in self.messages[-3:]],
|
||||||
|
)
|
||||||
self._idle = False
|
self._idle = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -177,6 +196,18 @@ class SubAgentTask:
|
|||||||
if self.multi_agent_mode:
|
if self.multi_agent_mode:
|
||||||
self._pending_answer_question_id = self._peek_pending_question_id()
|
self._pending_answer_question_id = self._peek_pending_question_id()
|
||||||
|
|
||||||
|
# 调试:记录进入本轮模型调用前的上下文摘要
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_model_call_start",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
turn=turn,
|
||||||
|
message_count=len(self.messages),
|
||||||
|
last_user_message=self.messages[-1].get("content", "")[:300] if self.messages and self.messages[-1].get("role") == "user" else "",
|
||||||
|
pending_answer_question_id=self._pending_answer_question_id,
|
||||||
|
)
|
||||||
|
|
||||||
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
||||||
if usage:
|
if usage:
|
||||||
self._apply_usage(usage)
|
self._apply_usage(usage)
|
||||||
@ -191,12 +222,14 @@ class SubAgentTask:
|
|||||||
if tool_calls:
|
if tool_calls:
|
||||||
final_message["tool_calls"] = tool_calls
|
final_message["tool_calls"] = tool_calls
|
||||||
self.messages.append(final_message)
|
self.messages.append(final_message)
|
||||||
|
self._persist_conversation(partial_summary=assistant_message[:200])
|
||||||
|
|
||||||
if not tool_calls:
|
if not tool_calls:
|
||||||
# 多智能体模式:没有 tool_calls 表示本轮结束,进入 idle 等待
|
# 多智能体模式:没有 tool_calls 表示本轮结束,进入 idle 等待
|
||||||
if self.multi_agent_mode:
|
if self.multi_agent_mode:
|
||||||
self._mark_idle()
|
self._mark_idle()
|
||||||
self._idle = True
|
self._idle = True
|
||||||
|
self._persist_conversation(partial_summary=assistant_message[:200])
|
||||||
continue
|
continue
|
||||||
# 普通模式:prompt 并要求继续 / finish_task
|
# 普通模式:prompt 并要求继续 / finish_task
|
||||||
self.messages.append({
|
self.messages.append({
|
||||||
@ -230,6 +263,7 @@ class SubAgentTask:
|
|||||||
"tool_call_id": tool_call.get("id", progress_id),
|
"tool_call_id": tool_call.get("id", progress_id),
|
||||||
"content": content,
|
"content": content,
|
||||||
})
|
})
|
||||||
|
self._persist_conversation(partial_summary=assistant_message[:200])
|
||||||
|
|
||||||
# 循环结束(取消或 idle 被外部终止)后的清理
|
# 循环结束(取消或 idle 被外部终止)后的清理
|
||||||
if self.multi_agent_mode and self._cancelled:
|
if self.multi_agent_mode and self._cancelled:
|
||||||
@ -237,7 +271,7 @@ class SubAgentTask:
|
|||||||
self.multi_agent_state.mark_status(self.agent_id, "terminated")
|
self.multi_agent_state.mark_status(self.agent_id, "terminated")
|
||||||
|
|
||||||
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 消息,并写入进度文件供前端查看。"""
|
||||||
if not self.multi_agent_state:
|
if not self.multi_agent_state:
|
||||||
return
|
return
|
||||||
# 如果这是对 pending 提问的回答,不走主对话转发,而是返回到 ask 工具结果
|
# 如果这是对 pending 提问的回答,不走主对话转发,而是返回到 ask 工具结果
|
||||||
@ -251,17 +285,58 @@ class SubAgentTask:
|
|||||||
inst = self.multi_agent_state.get_instance(self.agent_id)
|
inst = self.multi_agent_state.get_instance(self.agent_id)
|
||||||
if inst:
|
if inst:
|
||||||
inst.last_output = output_text[:500]
|
inst.last_output = output_text[:500]
|
||||||
|
# 写入进度文件,前端子智能体进度弹窗可直接展示
|
||||||
|
self.emit("progress", {
|
||||||
|
"subtype": "output",
|
||||||
|
"content": output_text,
|
||||||
|
"is_final": is_final,
|
||||||
|
"ts": int(time.time() * 1000),
|
||||||
|
})
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_output_forwarded",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
is_final=is_final,
|
||||||
|
content_preview=output_text[:300],
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"[SubAgentTask] forward output to master failed: {exc}")
|
logger.warning(f"[SubAgentTask] forward output to master failed: {exc}")
|
||||||
|
|
||||||
def _mark_idle(self) -> None:
|
def _mark_idle(self) -> None:
|
||||||
"""多智能体模式下,子智能体自然结束即本轮任务结束,进入 idle 状态。"""
|
"""多智能体模式下,子智能体自然结束即本轮任务结束,进入 idle 状态。"""
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_mark_idle",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
)
|
||||||
if self.multi_agent_state:
|
if self.multi_agent_state:
|
||||||
self.multi_agent_state.mark_status(self.agent_id, "idle")
|
self.multi_agent_state.mark_status(self.agent_id, "idle")
|
||||||
|
|
||||||
def inject_message(self, message_text: str) -> None:
|
def inject_message(self, message_text: str) -> None:
|
||||||
"""外部向子智能体上下文插入 user 消息,并唤醒 idle 状态。"""
|
"""外部向子智能体上下文插入 user 消息,并唤醒 idle 状态。"""
|
||||||
self.messages.append({"role": "user", "content": message_text})
|
self.messages.append({"role": "user", "content": message_text})
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_message_injected",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
message_preview=str(message_text)[:500],
|
||||||
|
was_idle=self._idle,
|
||||||
|
)
|
||||||
|
# inject_message 可能从其他线程(主对话线程)调用,需要线程安全唤醒。
|
||||||
|
# 优先使用子智能体 Task 所属事件循环投递 set(),避免跨线程直接操作 Future。
|
||||||
|
if self._continue_event is None:
|
||||||
|
self._continue_event = asyncio.Event()
|
||||||
|
if self._task is not None:
|
||||||
|
try:
|
||||||
|
task_loop = self._task.get_loop()
|
||||||
|
if task_loop.is_running():
|
||||||
|
task_loop.call_soon_threadsafe(self._continue_event.set)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self._continue_event.set()
|
self._continue_event.set()
|
||||||
|
|
||||||
def _peek_pending_question_id(self) -> Optional[str]:
|
def _peek_pending_question_id(self) -> Optional[str]:
|
||||||
@ -409,6 +484,14 @@ class SubAgentTask:
|
|||||||
if name == "ask_master":
|
if name == "ask_master":
|
||||||
question = str(args.get("question") or "").strip()
|
question = str(args.get("question") or "").strip()
|
||||||
question_id = str(args.get("question_id") or f"ask_master_{uuid.uuid4().hex[:10]}")
|
question_id = str(args.get("question_id") or f"ask_master_{uuid.uuid4().hex[:10]}")
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_tool_ask_master",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
question_id=question_id,
|
||||||
|
question=question[:500],
|
||||||
|
)
|
||||||
if not question:
|
if not question:
|
||||||
return {"success": False, "error": "question 不能为空"}
|
return {"success": False, "error": "question 不能为空"}
|
||||||
# 插入到主对话
|
# 插入到主对话
|
||||||
@ -428,6 +511,15 @@ class SubAgentTask:
|
|||||||
target_id = int(args.get("target_agent_id") or 0)
|
target_id = int(args.get("target_agent_id") or 0)
|
||||||
question = str(args.get("question") or "").strip()
|
question = str(args.get("question") or "").strip()
|
||||||
question_id = str(args.get("question_id") or f"ask_other_{uuid.uuid4().hex[:10]}")
|
question_id = str(args.get("question_id") or f"ask_other_{uuid.uuid4().hex[:10]}")
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_tool_ask_other_agent",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
target_agent_id=target_id,
|
||||||
|
question_id=question_id,
|
||||||
|
question=question[:500],
|
||||||
|
)
|
||||||
if not target_id or not question:
|
if not target_id or not question:
|
||||||
return {"success": False, "error": "参数缺失"}
|
return {"success": False, "error": "参数缺失"}
|
||||||
# 查找目标实例
|
# 查找目标实例
|
||||||
@ -516,10 +608,75 @@ class SubAgentTask:
|
|||||||
|
|
||||||
async def _write_failure(self, message: str, *, max_turns_exceeded: bool = False, timeout: bool = False) -> None:
|
async def _write_failure(self, message: str, *, max_turns_exceeded: bool = False, timeout: bool = False) -> None:
|
||||||
elapsed = time.time() - (self.stats["runtime_start"] / 1000)
|
elapsed = time.time() - (self.stats["runtime_start"] / 1000)
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_write_failure",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
message=message,
|
||||||
|
max_turns_exceeded=max_turns_exceeded,
|
||||||
|
timeout=timeout,
|
||||||
|
idle=self._idle,
|
||||||
|
cancelled=self._cancelled,
|
||||||
|
)
|
||||||
self._finalize_task(False, message, elapsed, max_turns_exceeded=max_turns_exceeded, timeout=timeout)
|
self._finalize_task(False, message, elapsed, max_turns_exceeded=max_turns_exceeded, timeout=timeout)
|
||||||
|
|
||||||
|
def _persist_conversation(self, *, partial_summary: str = "") -> None:
|
||||||
|
"""每轮结束后立即落盘子智能体对话,避免跑完了才存一次导致中间状态丢失。"""
|
||||||
|
try:
|
||||||
|
runtime_seconds = int((time.time() * 1000 - self.stats["runtime_start"]) / 1000)
|
||||||
|
status = "running"
|
||||||
|
if self._cancelled:
|
||||||
|
status = "terminated"
|
||||||
|
elif self.multi_agent_mode and self._idle:
|
||||||
|
status = "idle"
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_persist_conversation",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
status=status,
|
||||||
|
idle=self._idle,
|
||||||
|
cancelled=self._cancelled,
|
||||||
|
multi_agent_mode=self.multi_agent_mode,
|
||||||
|
)
|
||||||
|
conversation_data = {
|
||||||
|
"agent_id": self.agent_id,
|
||||||
|
"task_id": self.task_id,
|
||||||
|
"created_at": datetime.fromtimestamp(self.stats["runtime_start"] / 1000).isoformat(),
|
||||||
|
"updated_at": datetime.now().isoformat(),
|
||||||
|
"status": status,
|
||||||
|
"success": None,
|
||||||
|
"summary": partial_summary,
|
||||||
|
"messages": self.messages,
|
||||||
|
"stats": {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)},
|
||||||
|
}
|
||||||
|
self.conversation_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.conversation_file.write_text(json.dumps(conversation_data, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
output_data = {
|
||||||
|
"success": None,
|
||||||
|
"status": status,
|
||||||
|
"summary": partial_summary,
|
||||||
|
"stats": conversation_data["stats"],
|
||||||
|
}
|
||||||
|
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"[SubAgentTask] 增量保存失败: {exc}")
|
||||||
|
|
||||||
def _finalize_task(self, success: bool, summary: str, elapsed: float, *, max_turns_exceeded: bool = False, timeout: bool = False) -> None:
|
def _finalize_task(self, success: bool, summary: str, elapsed: float, *, max_turns_exceeded: bool = False, timeout: bool = False) -> None:
|
||||||
runtime_seconds = int(elapsed)
|
runtime_seconds = int(elapsed)
|
||||||
|
ma_debug(
|
||||||
|
"sub_agent_finalize_task",
|
||||||
|
task_id=self.task_id,
|
||||||
|
agent_id=self.agent_id,
|
||||||
|
display_name=self.display_name,
|
||||||
|
success=success,
|
||||||
|
summary=summary,
|
||||||
|
idle=self._idle,
|
||||||
|
cancelled=self._cancelled,
|
||||||
|
)
|
||||||
output_data = {
|
output_data = {
|
||||||
"success": success,
|
"success": success,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
|
|||||||
@ -826,13 +826,13 @@ def _has_pending_completion_work(*, web_terminal, conversation_id: str) -> bool:
|
|||||||
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)实例时继续轮询
|
# 多智能体模式:只要有未消费的主对话消息,或还有未结束(非 idle/terminated)的实例,就继续轮询。
|
||||||
if getattr(web_terminal, "multi_agent_mode", False) and has_running_ma:
|
# 即使全部实例都已 idle,只要 pending 消息尚未消费,也要继续,避免主任务结束后通知丢失。
|
||||||
|
if getattr(web_terminal, "multi_agent_mode", False):
|
||||||
state = sub_manager.get_multi_agent_state(conversation_id)
|
state = sub_manager.get_multi_agent_state(conversation_id)
|
||||||
if state:
|
if state:
|
||||||
if state.has_pending_master_messages():
|
if state.has_pending_master_messages():
|
||||||
return True
|
return True
|
||||||
# 只要还有非 idle 实例就继续;全部 idle 且无 pending 则结束轮询
|
|
||||||
if any(a.status not in {"idle", "terminated"} for a in state.list_all()):
|
if any(a.status not in {"idle", "terminated"} for a in state.list_all()):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -1839,6 +1839,7 @@ async def handle_task_with_sender(
|
|||||||
# 检查是否有后台运行的子智能体或待通知的完成任务
|
# 检查是否有后台运行的子智能体或待通知的完成任务
|
||||||
manager = getattr(web_terminal, "sub_agent_manager", None)
|
manager = getattr(web_terminal, "sub_agent_manager", None)
|
||||||
has_running_sub_agents = False
|
has_running_sub_agents = False
|
||||||
|
has_running_multi_agent = False
|
||||||
bg_manager = getattr(web_terminal, "background_command_manager", None)
|
bg_manager = getattr(web_terminal, "background_command_manager", None)
|
||||||
has_running_background_commands = False
|
has_running_background_commands = False
|
||||||
if manager:
|
if manager:
|
||||||
@ -1848,16 +1849,33 @@ async def handle_task_with_sender(
|
|||||||
debug_log(f"[SubAgent] reconcile_task_states failed: {exc}")
|
debug_log(f"[SubAgent] reconcile_task_states failed: {exc}")
|
||||||
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 消息并触发 Team Leader 响应。
|
||||||
|
if getattr(web_terminal, "multi_agent_mode", False):
|
||||||
|
for task in manager.tasks.values():
|
||||||
|
if (
|
||||||
|
isinstance(task, dict)
|
||||||
|
and task.get("conversation_id") == conversation_id
|
||||||
|
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()
|
||||||
if task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
|
if task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
|
||||||
and task.get("run_in_background")
|
and task.get("run_in_background")
|
||||||
|
and not task.get("multi_agent_mode")
|
||||||
and task.get("conversation_id") == conversation_id
|
and task.get("conversation_id") == conversation_id
|
||||||
]
|
]
|
||||||
pending_notice_tasks = [
|
pending_notice_tasks = [
|
||||||
task for task in manager.tasks.values()
|
task for task in manager.tasks.values()
|
||||||
if task.get("status") in TERMINAL_STATUSES.union({"terminated"})
|
if task.get("status") in TERMINAL_STATUSES.union({"terminated"})
|
||||||
and task.get("run_in_background")
|
and task.get("run_in_background")
|
||||||
|
and not task.get("multi_agent_mode")
|
||||||
and task.get("conversation_id") == conversation_id
|
and task.get("conversation_id") == conversation_id
|
||||||
and task.get("task_id") not in web_terminal._announced_sub_agent_tasks
|
and task.get("task_id") not in web_terminal._announced_sub_agent_tasks
|
||||||
]
|
]
|
||||||
@ -1884,11 +1902,11 @@ 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 合并为单一轮询链路,
|
# 统一「通知池」轮询器:子智能体 + 后台 run_command + 多智能体 pending 消息
|
||||||
# 每轮一次性取出两路所有待通知项,避免逐条触发「工作 → 停止 → 再工作」循环。
|
# 合并为单一轮询链路,每轮一次性取出所有待通知项,避免逐条触发「工作 → 停止 → 再工作」循环。
|
||||||
# 只 spawn 一个轮询器(无论是否同时存在子智能体与后台命令),从根本上消除
|
# 只 spawn 一个轮询器(无论是否同时存在子智能体与后台命令),从根本上消除
|
||||||
# 两个轮询器同时 create_chat_task 撞「单工作区互斥」的问题。
|
# 两个轮询器同时 create_chat_task 撞「单工作区互斥」的问题。
|
||||||
if has_running_sub_agents or has_running_background_commands:
|
if has_running_sub_agents or has_running_background_commands or has_running_multi_agent:
|
||||||
def run_completion_poll():
|
def run_completion_poll():
|
||||||
import asyncio
|
import asyncio
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from datetime import datetime
|
|||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
from modules.sub_agent import TERMINAL_STATUSES
|
from modules.sub_agent import TERMINAL_STATUSES
|
||||||
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
|
|
||||||
|
|
||||||
_VALID_SOURCES = {
|
_VALID_SOURCES = {
|
||||||
@ -329,6 +330,14 @@ def inject_multi_agent_master_message(
|
|||||||
if not raw:
|
if not raw:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
ma_debug(
|
||||||
|
"inject_multi_agent_master_message",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
text_preview=raw[:300],
|
||||||
|
inline=inline,
|
||||||
|
after_tool_call_id=after_tool_call_id,
|
||||||
|
)
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"runtime_injected": True,
|
"runtime_injected": True,
|
||||||
"source": "sub_agent",
|
"source": "sub_agent",
|
||||||
@ -405,6 +414,12 @@ async def process_multi_agent_master_messages(
|
|||||||
if not pending:
|
if not pending:
|
||||||
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(
|
||||||
|
"process_multi_agent_master_messages",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
count=len(pending),
|
||||||
|
previews=[str(m)[:200] for m in pending],
|
||||||
|
)
|
||||||
for msg in pending:
|
for msg in pending:
|
||||||
inject_multi_agent_master_message(
|
inject_multi_agent_master_message(
|
||||||
web_terminal=web_terminal,
|
web_terminal=web_terminal,
|
||||||
|
|||||||
@ -598,6 +598,7 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
|||||||
thinking_mode = data.get('thinking_mode') if preserve_mode and 'thinking_mode' in data else None
|
thinking_mode = data.get('thinking_mode') if preserve_mode and 'thinking_mode' in data else None
|
||||||
run_mode = data.get('mode') if preserve_mode and 'mode' in data else None
|
run_mode = data.get('mode') if preserve_mode and 'mode' in data else None
|
||||||
target_workspace_id = (data.get('workspace_id') or '').strip()
|
target_workspace_id = (data.get('workspace_id') or '').strip()
|
||||||
|
multi_agent_mode = bool(data.get('multi_agent_mode'))
|
||||||
|
|
||||||
if target_workspace_id:
|
if target_workspace_id:
|
||||||
try:
|
try:
|
||||||
@ -641,6 +642,7 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
|||||||
metadata_overrides={
|
metadata_overrides={
|
||||||
"permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
|
"permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
|
||||||
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
|
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
|
||||||
|
"multi_agent_mode": multi_agent_mode,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@ -654,7 +656,11 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
|||||||
"safe_navigation": True,
|
"safe_navigation": True,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
result = terminal.create_new_conversation(thinking_mode=thinking_mode, run_mode=run_mode)
|
result = terminal.create_new_conversation(
|
||||||
|
thinking_mode=thinking_mode,
|
||||||
|
run_mode=run_mode,
|
||||||
|
metadata_overrides={"multi_agent_mode": multi_agent_mode} if multi_agent_mode else None,
|
||||||
|
)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
# 仅在当前工作区创建时更新 session 模式;指定其他工作区时由前端切换后自动同步。
|
# 仅在当前工作区创建时更新 session 模式;指定其他工作区时由前端切换后自动同步。
|
||||||
|
|||||||
@ -122,15 +122,17 @@ export const actionMethods = {
|
|||||||
this.versioningInitializingBackupToastId = backupToastId;
|
this.versioningInitializingBackupToastId = backupToastId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/conversations', {
|
const isMultiAgent = Boolean(this.multiAgentMode);
|
||||||
|
const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations';
|
||||||
|
const createBody = isMultiAgent
|
||||||
|
? JSON.stringify({ preserve_mode: true, thinking_mode: this.thinkingMode, mode: this.runMode })
|
||||||
|
: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode });
|
||||||
|
const response = await fetch(createUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: createBody
|
||||||
thinking_mode: this.thinkingMode,
|
|
||||||
mode: this.runMode
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|||||||
@ -209,15 +209,17 @@ export const sendMethods = {
|
|||||||
this.versioningInitializingBackupToastId = backupToastId;
|
this.versioningInitializingBackupToastId = backupToastId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createResp = await fetch('/api/conversations', {
|
const isMultiAgent = Boolean(this.multiAgentMode);
|
||||||
|
const createUrl = isMultiAgent ? '/api/multiagent/conversations' : '/api/conversations';
|
||||||
|
const createBody = isMultiAgent
|
||||||
|
? JSON.stringify({ preserve_mode: true, thinking_mode: this.thinkingMode, mode: this.runMode })
|
||||||
|
: JSON.stringify({ thinking_mode: this.thinkingMode, mode: this.runMode });
|
||||||
|
const createResp = await fetch(createUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: createBody
|
||||||
thinking_mode: this.thinkingMode,
|
|
||||||
mode: this.runMode
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
const createResult = await createResp.json().catch(() => ({}));
|
const createResult = await createResp.json().catch(() => ({}));
|
||||||
if (!createResp.ok || !createResult?.success || !createResult?.conversation_id) {
|
if (!createResp.ok || !createResult?.success || !createResult?.conversation_id) {
|
||||||
@ -258,7 +260,8 @@ export const sendMethods = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pathFragment = this.stripConversationPrefix(targetConversationId);
|
const pathFragment = this.stripConversationPrefix(targetConversationId);
|
||||||
history.replaceState({ conversationId: targetConversationId }, '', `/${pathFragment}`);
|
const urlPrefix = this.multiAgentMode ? '/multiagent/' : '/';
|
||||||
|
history.replaceState({ conversationId: targetConversationId }, '', `${urlPrefix}${pathFragment}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '发送失败',
|
title: '发送失败',
|
||||||
|
|||||||
@ -29,13 +29,29 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="subagent-activity-body">
|
<div class="subagent-activity-body">
|
||||||
<div v-if="activityError" class="subagent-activity-error">{{ activityError }}</div>
|
<div v-if="activityError" class="subagent-activity-error">{{ activityError }}</div>
|
||||||
<div v-else-if="!displayItems.length" class="subagent-activity-empty">
|
<div v-else-if="!timelineItems.length" class="subagent-activity-empty">
|
||||||
{{ activityLoading ? '正在读取子智能体活动...' : '暂无活动记录' }}
|
{{ activityLoading ? '正在读取子智能体活动...' : '暂无活动记录' }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="subagent-activity-list">
|
<div v-else class="subagent-activity-list">
|
||||||
<div v-for="item in displayItems" :key="item.key" class="subagent-activity-item">
|
<div
|
||||||
<span class="subagent-activity-text">{{ item.text }}</span>
|
v-for="item in timelineItems"
|
||||||
<span class="subagent-activity-state" :class="item.state">{{ item.stateLabel }}</span>
|
:key="item.key"
|
||||||
|
class="subagent-activity-item"
|
||||||
|
:class="{ 'subagent-output-item': item.kind === 'output', expanded: item.kind === 'output' && expandedOutputs.has(item.key) }"
|
||||||
|
@click="handleItemClick(item)"
|
||||||
|
>
|
||||||
|
<template v-if="item.kind === 'output'">
|
||||||
|
<div class="subagent-output-meta">
|
||||||
|
<span v-if="item.isFinal" class="subagent-output-final">结束汇报</span>
|
||||||
|
<span v-else class="subagent-output-progress">进度输出</span>
|
||||||
|
<span class="subagent-output-hint">{{ expandedOutputs.has(item.key) ? '点击收起' : '点击展开' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="subagent-output-content">{{ item.content }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="subagent-activity-text">{{ item.text }}</span>
|
||||||
|
<span class="subagent-activity-state" :class="item.state">{{ item.stateLabel }}</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -50,17 +66,22 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { useSubAgentStore } from '@/stores/subAgent';
|
import { useSubAgentStore } from '@/stores/subAgent';
|
||||||
|
|
||||||
type ActivityEntry = {
|
type ActivityEntry = {
|
||||||
|
type?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
tool?: string;
|
tool?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
args?: Record<string, any>;
|
args?: Record<string, any>;
|
||||||
ts?: number;
|
ts?: number;
|
||||||
|
subtype?: string;
|
||||||
|
content?: string;
|
||||||
|
is_final?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const subAgentStore = useSubAgentStore();
|
const subAgentStore = useSubAgentStore();
|
||||||
const { activeAgent, activityEntries, activityLoading, activityError, stoppingTaskIds } =
|
const { activeAgent, activityEntries, activityLoading, activityError, stoppingTaskIds } =
|
||||||
storeToRefs(subAgentStore);
|
storeToRefs(subAgentStore);
|
||||||
const stopError = ref('');
|
const stopError = ref('');
|
||||||
|
const expandedOutputs = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
subAgentStore.closeSubAgent();
|
subAgentStore.closeSubAgent();
|
||||||
@ -141,48 +162,138 @@ const isTerminalStatus = (status?: string) => {
|
|||||||
return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized);
|
return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized);
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayItems = computed(() => {
|
const toggleOutput = (key: string) => {
|
||||||
|
const next = new Set(expandedOutputs.value);
|
||||||
|
if (next.has(key)) {
|
||||||
|
next.delete(key);
|
||||||
|
} else {
|
||||||
|
next.add(key);
|
||||||
|
}
|
||||||
|
expandedOutputs.value = next;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemClick = (item: any) => {
|
||||||
|
if (item.kind === 'output') {
|
||||||
|
toggleOutput(item.key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timelineItems = computed(() => {
|
||||||
const entries = activityEntries.value || [];
|
const entries = activityEntries.value || [];
|
||||||
const groups: { key: string; entry: ActivityEntry }[] = [];
|
const rawItems: { kind: 'tool'; key: string; entry: ActivityEntry } | { kind: 'output'; key: string; content: string; isFinal: boolean }[] = [];
|
||||||
|
let currentToolGroup: { kind: 'tool'; key: string; entry: ActivityEntry } | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < entries.length; i++) {
|
const flushToolGroup = () => {
|
||||||
const entry = entries[i];
|
if (currentToolGroup) {
|
||||||
if (!entry || entry.type !== 'progress') continue;
|
rawItems.push(currentToolGroup);
|
||||||
|
currentToolGroup = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const baseKey = entry.id || `${entry.tool || 'tool'}-${entry.ts || i}`;
|
entries.forEach((entry: ActivityEntry, index: number) => {
|
||||||
const lastGroup = groups[groups.length - 1];
|
if (entry?.type === 'progress' && entry?.subtype === 'output' && typeof entry.content === 'string') {
|
||||||
|
flushToolGroup();
|
||||||
// 同一个 id 的相邻进度更新(running -> completed)合并为一次工具调用。
|
rawItems.push({
|
||||||
// 如果上一个同 id 的组已经进入终态,则后续同 id 的记录视为新的工具调用。
|
kind: 'output',
|
||||||
if (
|
key: `output-${entry.ts || index}`,
|
||||||
lastGroup &&
|
content: entry.content,
|
||||||
(lastGroup.entry.id === entry.id || lastGroup.key === baseKey) &&
|
isFinal: !!entry.is_final,
|
||||||
!isTerminalStatus(lastGroup.entry.status)
|
});
|
||||||
) {
|
return;
|
||||||
lastGroup.entry = { ...lastGroup.entry, ...entry };
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成唯一 key:id 重复时加上序号
|
if (!entry || entry.type !== 'progress' || !entry.tool) return;
|
||||||
|
|
||||||
|
const baseKey = entry.id || `${entry.tool}-${entry.ts || index}`;
|
||||||
|
if (
|
||||||
|
currentToolGroup &&
|
||||||
|
(currentToolGroup.entry.id === entry.id || currentToolGroup.key === baseKey) &&
|
||||||
|
!isTerminalStatus(currentToolGroup.entry.status)
|
||||||
|
) {
|
||||||
|
currentToolGroup.entry = { ...currentToolGroup.entry, ...entry };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushToolGroup();
|
||||||
let key = baseKey;
|
let key = baseKey;
|
||||||
let suffix = 0;
|
let suffix = 0;
|
||||||
while (groups.some((g) => g.key === key)) {
|
while (rawItems.some((item) => item.kind === 'tool' && item.key === key)) {
|
||||||
suffix++;
|
suffix++;
|
||||||
key = `${baseKey}--${suffix}`;
|
key = `${baseKey}--${suffix}`;
|
||||||
}
|
}
|
||||||
groups.push({ key, entry: { ...entry } });
|
currentToolGroup = { kind: 'tool', key, entry: { ...entry } };
|
||||||
}
|
});
|
||||||
|
|
||||||
return groups.map((group) => {
|
flushToolGroup();
|
||||||
const item = group.entry;
|
|
||||||
const state = normalizeStatus(item.status);
|
return rawItems.map((item) => {
|
||||||
const stateLabel = state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中';
|
if (item.kind === 'output') return item;
|
||||||
|
const state = normalizeStatus(item.entry.status);
|
||||||
return {
|
return {
|
||||||
key: group.key,
|
kind: 'tool' as const,
|
||||||
|
key: item.key,
|
||||||
state,
|
state,
|
||||||
stateLabel,
|
stateLabel: state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中',
|
||||||
text: buildText(item)
|
text: buildText(item.entry)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.subagent-output-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.subagent-output-title,
|
||||||
|
.subagent-activity-list-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.subagent-output-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.subagent-output-item {
|
||||||
|
background: var(--surface-soft);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.subagent-output-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.subagent-output-final {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.subagent-output-progress {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.subagent-output-hint {
|
||||||
|
margin-left: auto;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.subagent-output-content {
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
.subagent-output-item.expanded .subagent-output-content {
|
||||||
|
-webkit-line-clamp: unset;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1590,7 +1590,8 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
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;
|
||||||
const hasRunningBackground = hasRunningSubAgents || hasRunningBackgroundCommands;
|
const hasRunningBackground = hasRunningSubAgents || hasRunningBackgroundCommands;
|
||||||
if (!hasRunningBackground) {
|
// 多智能体模式下,主任务停止即可恢复输入区,不因子智能体后台运行保持停止按钮
|
||||||
|
if (ctx.multiAgentMode || !hasRunningBackground) {
|
||||||
ctx.taskInProgress = false;
|
ctx.taskInProgress = false;
|
||||||
} else {
|
} else {
|
||||||
ctx.waitingForSubAgent = hasRunningSubAgents;
|
ctx.waitingForSubAgent = hasRunningSubAgents;
|
||||||
@ -1616,8 +1617,8 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
});
|
});
|
||||||
socketLog('任务完成', data);
|
socketLog('任务完成', data);
|
||||||
|
|
||||||
// 如果有运行中的子智能体,不重置任务状态
|
// 多智能体模式下,主任务完成即视为可发送状态,后台子智能体是否 idle/running 不影响输入区
|
||||||
if (!data.has_running_sub_agents) {
|
if (ctx.multiAgentMode || !data.has_running_sub_agents) {
|
||||||
console.log('[DEBUG] 没有运行中的子智能体,重置任务状态');
|
console.log('[DEBUG] 没有运行中的子智能体,重置任务状态');
|
||||||
if (ctx.waitingForSubAgent) {
|
if (ctx.waitingForSubAgent) {
|
||||||
ctx.waitingForSubAgent = false;
|
ctx.waitingForSubAgent = false;
|
||||||
@ -1652,13 +1653,16 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
});
|
});
|
||||||
socketLog('等待子智能体完成:', data);
|
socketLog('等待子智能体完成:', data);
|
||||||
|
|
||||||
// 设置标志:有子智能体在运行,阻止状态重置
|
// 多智能体模式下,子智能体 idle/running 是常态,不阻塞主对话输入区
|
||||||
ctx.waitingForSubAgent = true;
|
if (!ctx.multiAgentMode) {
|
||||||
|
// 设置标志:有子智能体在运行,阻止状态重置
|
||||||
|
ctx.waitingForSubAgent = true;
|
||||||
|
|
||||||
// 保持任务进行中状态
|
// 保持任务进行中状态
|
||||||
ctx.taskInProgress = true;
|
ctx.taskInProgress = true;
|
||||||
ctx.streamingMessage = false;
|
ctx.streamingMessage = false;
|
||||||
ctx.stopRequested = false;
|
ctx.stopRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[DEBUG] 当前状态 (after sub_agent_waiting):', {
|
console.log('[DEBUG] 当前状态 (after sub_agent_waiting):', {
|
||||||
taskInProgress: ctx.taskInProgress,
|
taskInProgress: ctx.taskInProgress,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user