"""子智能体任务管理(主进程内协程模式)。 子智能体不再作为独立子进程启动,而是作为 SubAgentManager 所在事件循环中的 asyncio.Task 运行。所有实际工具调用都通过主 WebTerminal 执行,因此自然复用 主进程的宿主机沙箱 / Docker 容器链路。 """ from __future__ import annotations import asyncio import json import threading import time from pathlib import Path, PurePosixPath from typing import Any, Dict, List, Optional, TYPE_CHECKING from config import ( OUTPUT_FORMATS, SUB_AGENT_DEFAULT_TIMEOUT, SUB_AGENT_MAX_ACTIVE, SUB_AGENT_MODELS_CONFIG_FILE, SUB_AGENT_STATUS_POLL_INTERVAL, ) from utils.logger import setup_logger from modules.sub_agent.task import SubAgentTask from modules.sub_agent.prompts import build_user_message, build_system_prompt from modules.sub_agent.tools import handle_search_workspace, handle_read_mediafile from modules.sub_agent.state import SubAgentStateMixin from modules.sub_agent.stats import SubAgentStatsMixin from modules.sub_agent.creation import SubAgentCreationMixin from modules.multi_agent.debug_logger import ma_debug from server.utils_common import debug_log if TYPE_CHECKING: from core.web_terminal import WebTerminal from modules.user_container_manager import ContainerHandle logger = setup_logger(__name__) TERMINAL_STATUSES = {"completed", "failed", "timeout"} class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMixin): """负责主智能体与子智能体的任务调度(协程模式)。""" def __init__( self, project_path: str, data_dir: str, container_session: Optional["ContainerHandle"] = None, ): self.project_path = Path(project_path).resolve() self.data_dir = Path(data_dir).resolve() # 子智能体任务和状态按 data_dir 隔离(web 模式下按用户/工作区自动隔离) self.base_dir = self.data_dir / "sub_agent_tasks" self.state_file = self.data_dir / "sub_agents.json" self.models_config_file = SUB_AGENT_MODELS_CONFIG_FILE self.container_session: Optional["ContainerHandle"] = container_session self.host_execution_mode: str = "sandbox" self.terminal: Optional["WebTerminal"] = None # 多智能体模式:为每个启用 multi_agent_mode 的会话维护一个 MultiAgentState # key = conversation_id, value = MultiAgentState self.multi_agent_states: Dict[str, Any] = {} self.base_dir.mkdir(parents=True, exist_ok=True) self.state_file.parent.mkdir(parents=True, exist_ok=True) self.tasks: Dict[str, Dict[str, Any]] = {} self.conversation_agents: Dict[str, List[int]] = {} self._running_tasks: Dict[str, asyncio.Task] = {} self._event_loop: Optional[asyncio.AbstractEventLoop] = None self._loop_thread: Optional[threading.Thread] = None self._state_lock = threading.Lock() # agent_id -> SubAgentTask 映射(供多智能体消息注入使用) self._sub_agent_instances: Dict[int, Any] = {} self._load_state() try: self.reconcile_task_states() except Exception: pass try: self.restore_running_tasks() except Exception: logger.exception("[SubAgentManager] 恢复运行中子智能体任务失败") pass # ------------------------------------------------------------------ # 生命周期与事件循环 # ------------------------------------------------------------------ def _ensure_event_loop(self) -> asyncio.AbstractEventLoop: """确保有一个独立的后台事件循环供子智能体使用。""" if self._event_loop is not None and not self._event_loop.is_closed(): return self._event_loop loop = asyncio.new_event_loop() self._event_loop = loop def run_loop(): asyncio.set_event_loop(loop) try: loop.run_forever() finally: try: loop.close() except Exception: pass thread = threading.Thread(target=run_loop, name="sub-agent-loop", daemon=True) thread.start() self._loop_thread = thread return loop async def _create_task(self, coro): """在事件循环内部把协程包装为 Task。""" return asyncio.create_task(coro) def _run_coro(self, coro): """在后台事件循环中调度一个协程并返回 asyncio.Task。""" loop = self._ensure_event_loop() # 先提交创建 Task 的协程,阻塞等待拿到 Task 句柄 future = asyncio.run_coroutine_threadsafe(self._create_task(coro), loop) return future.result(timeout=10) def set_terminal(self, terminal: "WebTerminal") -> None: """注入主终端引用,用于工具执行代理。""" self.terminal = terminal def set_container_session(self, session: Optional["ContainerHandle"]): """更新容器会话信息。""" self.container_session = session def set_host_execution_mode(self, mode: str) -> None: normalized = str(mode or "").strip().lower() self.host_execution_mode = "direct" if normalized == "direct" else "sandbox" # ------------------------------------------------------------------ # 公共方法 # ------------------------------------------------------------------ def create_sub_agent( self, *, agent_id: int, summary: str, task: str, deliverables_dir: Optional[str] = None, timeout_seconds: Optional[int] = None, conversation_id: Optional[str] = None, run_in_background: bool = False, model_key: Optional[str] = None, thinking_mode: Optional[str] = None, multi_agent_mode: bool = False, role_id: Optional[str] = None, display_name: Optional[str] = None, system_prompt: Optional[str] = None, task_message: Optional[str] = None, compress_threshold_tokens: Optional[int] = None, ) -> Dict: """创建子智能体任务并启动协程。 参数 multi_agent_mode: True 时启用多智能体模式。 参数 role_id: 多智能体模式下的角色标诶。 参数 display_name: 多智能体模式下的显示名(如 UI Operator_1)。 """ validation_error = self._validate_create_params(agent_id, summary, task, deliverables_dir, multi_agent_mode=multi_agent_mode) if validation_error: return {"success": False, "error": validation_error} if not thinking_mode: return {"success": False, "error": "缺少 thinking_mode 参数,必须指定 fast 或 thinking"} if thinking_mode not in {"fast", "thinking"}: return {"success": False, "error": "thinking_mode 仅支持 fast 或 thinking"} if not conversation_id: return {"success": False, "error": "缺少对话ID,无法创建子智能体"} if not self._ensure_agent_slot_available(conversation_id, agent_id): return { "success": False, "error": f"该对话已使用过编号 {agent_id},请更换新的子智能体代号。" } if self._active_task_count(conversation_id) >= SUB_AGENT_MAX_ACTIVE: return { "success": False, "error": f"该对话已存在 {SUB_AGENT_MAX_ACTIVE} 个运行中的子智能体,请稍后再试。", } task_id = self._generate_task_id(agent_id) task_root = self.base_dir / task_id task_root.mkdir(parents=True, exist_ok=True) try: deliverables_path = self._resolve_deliverables_dir(deliverables_dir, multi_agent_mode=multi_agent_mode) except ValueError as exc: return {"success": False, "error": str(exc)} task_file = task_root / "task.txt" system_prompt_file = task_root / "system_prompt.txt" output_file = task_root / "output.json" stats_file = task_root / "stats.json" progress_file = task_root / "progress.jsonl" conversation_file = task_root / "conversation.json" prompt_workspace = self._get_runtime_path(self.project_path) deliverables_display = self._get_runtime_path(deliverables_path) if task_message: user_message = task_message else: 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") if system_prompt: final_system_prompt = system_prompt else: final_system_prompt = build_system_prompt(prompt_workspace) system_prompt_file.write_text(final_system_prompt, encoding="utf-8") # timeout_seconds 为 None 表示永久子智能体(不会被时间终结) task_record = { "task_id": task_id, "agent_id": agent_id, "summary": summary, "task": task, "status": "running", "deliverables_dir": str(deliverables_path), "timeout_seconds": timeout_seconds, "thinking_mode": thinking_mode, "created_at": time.time(), "updated_at": time.time(), "conversation_id": conversation_id, "run_in_background": run_in_background, "multi_agent_mode": bool(multi_agent_mode), "task_root": str(task_root), "output_file": str(output_file), "stats_file": str(stats_file), "progress_file": str(progress_file), "conversation_file": str(conversation_file), "model_key": model_key, "role_id": role_id, "display_name": display_name, "execution_mode": "in_process", "compress_threshold_tokens": compress_threshold_tokens, "container_name": None, } self.tasks[task_id] = task_record self._mark_agent_id_used(conversation_id, agent_id) self._save_state() # 多智能体模式:为该会话创建或复用 MultiAgentState multi_agent_state = None if multi_agent_mode: multi_agent_state = self.get_or_create_multi_agent_state(conversation_id) # 把实例注册到 state from modules.multi_agent.state import AgentInstance inst = AgentInstance( agent_id=agent_id, role_id=role_id or "", display_name=display_name or f"Agent_{agent_id}", task_id=task_id, status="running", summary=summary, ) try: multi_agent_state.register_instance(inst) except ValueError: return {"success": False, "error": f"agent_id {agent_id} 已在该会话中使用"} sub_agent = SubAgentTask( manager=self, task_record=task_record, task_message=user_message, system_prompt=final_system_prompt, model_key=model_key, thinking_mode=thinking_mode, multi_agent_mode=multi_agent_mode, multi_agent_state=multi_agent_state, 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() asyncio_task = self._run_coro(task_coro) sub_agent._task = asyncio_task self._running_tasks[task_id] = asyncio_task # 缓存 sub_agent 实例供给多智能体模式 Poli注入使用 self._sub_agent_instances[agent_id] = sub_agent def _on_done(fut): try: self._running_tasks.pop(task_id, None) self._sub_agent_instances.pop(agent_id, None) self.reconcile_task_states(conversation_id=conversation_id) # 多智能体模式:结束时把状态写回 MultiAgentState if multi_agent_mode and multi_agent_state: 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) message = f"子智能体{agent_id} 已创建,任务ID: {task_id}" if multi_agent_mode and display_name: message = f"{display_name} 已创建,任务ID: {task_id}" 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 { "success": True, "task_id": task_id, "agent_id": agent_id, "status": "running", "message": message, "deliverables_dir": str(deliverables_path), "run_in_background": run_in_background, "display_name": display_name, } def wait_for_completion( self, *, task_id: Optional[str] = None, agent_id: Optional[int] = None, timeout_seconds: Optional[int] = None, ) -> Dict: """阻塞等待子智能体完成或超时。""" task = self._select_task(task_id, agent_id) if not task: return {"success": False, "error": "未找到对应的子智能体任务"} if task.get("status") in TERMINAL_STATUSES or task.get("status") == "terminated": if task.get("final_result"): return task["final_result"] return {"success": False, "status": task.get("status"), "message": "子智能体已结束。"} real_task_id = task["task_id"] deadline = time.time() + (timeout_seconds or task.get("timeout_seconds") or SUB_AGENT_DEFAULT_TIMEOUT) while time.time() < deadline: self.reconcile_task_states() # 关键:其他线程(如前端轮询 /api/sub_agents)可能调用 _load_state() # 并替换 self.tasks 字典,导致旧 task 引用失效。每次循环重新获取引用。 task = self.tasks.get(real_task_id) if not task: return {"success": False, "error": "未找到对应的子智能体任务"} running_task = self._running_tasks.get(real_task_id) status = task.get("status") # 已到达终态:返回最终结果(持续 reconcile 直到 final_result 就绪) if status in TERMINAL_STATUSES or status == "terminated": if task.get("final_result"): return task["final_result"] # 终态但 final_result 尚未写入,短暂等待后重试 time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL) self.reconcile_task_states() task = self.tasks.get(real_task_id) or task if task.get("final_result"): return task["final_result"] return {"success": False, "status": status, "message": "子智能体已结束,但未获取到结果。"} # asyncio Task 已结束但状态可能还没同步:等待 final_result 就绪 if running_task and running_task.done(): self.reconcile_task_states() task = self.tasks.get(real_task_id) or task if task.get("final_result"): return task["final_result"] # 结果尚未落盘,继续轮询,避免把「已创建」误判为失败 time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL) continue time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL) return self._handle_timeout(task) def soft_stop_all_agents(self, conversation_id: str) -> int: """软停止指定会话的所有运行中子智能体。 与 terminate_sub_agent 的区别:不取消 asyncio.Task 而是设 _soft_stop 标志, 让子智能体在当前工具完成后进入 idle 状态,保留上下文。 返回实际发出软停止信号的子智能体数量。 """ ma_debug("soft_stop_all_agents_enter", conversation_id=conversation_id) count = 0 matched = 0 skipped_terminal = 0 skipped_no_instance = 0 for task_id, task_info in list(self.tasks.items()): if task_info.get("conversation_id") != conversation_id: continue matched += 1 status = task_info.get("status") ma_debug( "soft_stop_iter", task_id=task_id, status=status, ) if status in TERMINAL_STATUSES.union({"terminated", "idle"}): skipped_terminal += 1 continue agent_id = task_info.get("agent_id") if agent_id is None: continue # 從 _sub_agent_instances 查找 SubAgentTask 实例 sub_agent_task = self._sub_agent_instances.get(agent_id) if sub_agent_task and hasattr(sub_agent_task, "request_soft_stop"): try: sub_agent_task.request_soft_stop() count += 1 except Exception as exc: ma_debug("soft_stop_failed", task_id=task_id, error=str(exc)) else: skipped_no_instance += 1 ma_debug( "soft_stop_all_agents_done", conversation_id=conversation_id, matched=matched, count=count, skipped_terminal=skipped_terminal, skipped_no_instance=skipped_no_instance, ) return count def terminate_sub_agent( self, *, task_id: Optional[str] = None, agent_id: Optional[int] = None, ) -> Dict: """强制关闭指定子智能体。""" task = self._select_task(task_id, agent_id) if not task: return {"success": False, "error": "未找到对应的子智能体任务"} task_id = task["task_id"] running_task = self._running_tasks.pop(task_id, None) if running_task and not running_task.done(): # 子智能体运行在独立事件循环线程中,取消操作必须投递到该循环 try: loop = running_task.get_loop() loop.call_soon_threadsafe(running_task.cancel) except Exception: running_task.cancel() deadline = time.time() + 5 while not running_task.done() and time.time() < deadline: time.sleep(0.05) self._mark_task_terminated( task, message="子智能体已被强制关闭。", system_message=f"🛑 子智能体{task.get('agent_id')} 已被手动关闭。", notified=True, ) self._save_state() return { "success": True, "task_id": task_id, "message": "子智能体已被强制关闭。", "system_message": f"🛑 子智能体{task.get('agent_id')} 已被手动关闭。", } def get_sub_agent_status( self, *, agent_ids: Optional[List[int]] = None, ) -> Dict: """获取指定子智能体的详细状态。 对于已结束(completed/failed/timeout/terminated)的子智能体,同样返回其 最终状态,而不是返回「不存在」。 """ if not agent_ids: return {"success": False, "error": "必须指定至少一个agent_id"} def _find_task_by_agent_id(aid: int): # 先查运行中/待运行的任务 task = self._select_task(None, aid) if task: return task # 再查已结束的任务(按创建时间取最新一条) candidates = [ t for t in self.tasks.values() if t.get("agent_id") == aid ] if not candidates: return None candidates.sort(key=lambda item: item.get("created_at", 0), reverse=True) return candidates[0] results = [] for agent_id in agent_ids: task = _find_task_by_agent_id(agent_id) if not task: results.append({ "agent_id": agent_id, "found": False, "error": "子智能体不存在", }) continue status = task.get("status") if status not in TERMINAL_STATUSES.union({"terminated"}): self._check_task_status(task) status = task.get("status") stats = {} stats_file = Path(task.get("stats_file", "")) if stats_file.exists(): try: stats = json.loads(stats_file.read_text(encoding="utf-8")) except Exception: pass stats_summary = self._build_stats_summary(stats) results.append({ "agent_id": agent_id, "found": True, "task_id": task["task_id"], "status": status, "summary": task.get("summary"), "created_at": task.get("created_at"), "updated_at": task.get("updated_at"), "deliverables_dir": task.get("deliverables_dir"), "stats": stats, "stats_summary": stats_summary, "final_result": task.get("final_result"), }) return {"success": True, "results": results} def poll_updates(self) -> List[Dict]: """检查运行中的子智能体任务,返回新完成的结果。""" updates: List[Dict] = [] self.reconcile_task_states() pending_tasks = [ task for task in self.tasks.values() if task.get("status") not in TERMINAL_STATUSES.union({"terminated"}) ] if not pending_tasks: return updates state_changed = False for task in pending_tasks: result = self._check_task_status(task) if result["status"] in TERMINAL_STATUSES: updates.append(result) state_changed = True if state_changed: self._save_state() return updates def lookup_task(self, *, task_id: Optional[str] = None, agent_id: Optional[int] = None) -> Optional[Dict]: """只读查询任务信息。""" task = self._select_task(task_id, agent_id) if not task: return None return { "task_id": task.get("task_id"), "agent_id": task.get("agent_id"), "status": task.get("status"), "timeout_seconds": task.get("timeout_seconds"), "conversation_id": task.get("conversation_id"), } def get_overview(self, conversation_id: Optional[str] = None) -> List[Dict[str, Any]]: """返回子智能体任务概览,用于前端展示。""" self.reconcile_task_states(conversation_id=conversation_id) overview: List[Dict[str, Any]] = [] for task_id, task in self.tasks.items(): if conversation_id and task.get("conversation_id") != conversation_id: continue snapshot = { "task_id": task_id, "agent_id": task.get("agent_id"), "summary": task.get("summary"), "status": task.get("status"), "display_name": task.get("display_name") or "", "created_at": task.get("created_at"), "updated_at": task.get("updated_at"), "target_dir": task.get("target_project_dir"), "last_tool": task.get("last_tool"), "deliverables_dir": task.get("deliverables_dir"), "copied_path": task.get("copied_path"), "conversation_id": task.get("conversation_id"), "sub_conversation_id": task.get("sub_conversation_id"), } # 读取 stats 文件获取当前上下文 token stats_file = Path(task.get("stats_file", "")) if stats_file.exists(): try: stats = json.loads(stats_file.read_text(encoding="utf-8")) snapshot["current_context_tokens"] = stats.get("current_context_tokens", 0) snapshot["stats_summary"] = self._build_stats_summary(stats) except Exception: snapshot["current_context_tokens"] = 0 else: snapshot["current_context_tokens"] = 0 if snapshot["status"] in TERMINAL_STATUSES or snapshot["status"] == "terminated": final_result = task.get("final_result") or {} snapshot["final_message"] = final_result.get("system_message") or final_result.get("message") snapshot["success"] = final_result.get("success") overview.append(snapshot) overview.sort(key=lambda item: item.get("created_at") or 0, reverse=True) return overview # ------------------------------------------------------------------ # 工具执行代理 # ------------------------------------------------------------------ async def execute_tool_for_sub_agent(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: """代表子智能体在主进程中执行工具。""" if not self.terminal: return {"success": False, "error": "子智能体管理器未绑定终端,无法执行工具"} try: # 多智能体模式常见问答工具已在 SubAgentTask._execute_multi_agent_tool 中处理 # 这里只处理实际通过主进程执行的工具 if tool_name == "search_workspace": return await handle_search_workspace(self.project_path, self.terminal, arguments) if tool_name == "read_mediafile": return await handle_read_mediafile(self.project_path, arguments) # 其余工具直接走主进程 handle_tool_call,自然经过沙箱/容器/权限链路 result_text = await self.terminal.handle_tool_call(tool_name, arguments) try: return json.loads(result_text) except Exception: return {"success": True, "output": result_text} except Exception as exc: logger.exception(f"[SubAgent] 工具执行异常: {tool_name}") return {"success": False, "error": f"工具执行异常: {exc}"} # ------------------------------------------------------------------ # 重启后恢复运行中任务 # ------------------------------------------------------------------ def restore_running_tasks(self) -> int: """程序重启后,从 conversation.json 恢复非终态子智能体任务并重新运行。 返回成功恢复的任务数。 """ from modules.sub_agent.task import SubAgentTask restored = 0 terminal_statuses = TERMINAL_STATUSES.union({"terminated"}) for task_id, task in list(self.tasks.items()): if not isinstance(task, dict): continue # 仅恢复多智能体模式任务;传统子智能体保持原有清理逻辑 if not task.get("multi_agent_mode"): continue status = task.get("status", "running") if status in terminal_statuses: continue # 已在内存中运行,无需恢复 if task_id in self._running_tasks: continue task_root = Path(task.get("task_root", "")) conversation_file = Path(task.get("conversation_file", "")) system_prompt_file = task_root / "system_prompt.txt" task_message_file = task_root / "task.txt" if not conversation_file.exists(): logger.warning(f"[restore] 任务 {task_id} 的对话文件缺失,无法恢复") continue try: conversation_data = json.loads(conversation_file.read_text(encoding="utf-8")) messages = list(conversation_data.get("messages") or []) except Exception as exc: logger.warning(f"[restore] 读取任务 {task_id} 对话文件失败: {exc}") continue system_prompt = "" if system_prompt_file.exists(): try: system_prompt = system_prompt_file.read_text(encoding="utf-8") except Exception: pass task_message = "" if task_message_file.exists(): try: task_message = task_message_file.read_text(encoding="utf-8") except Exception: pass # 如果对话历史为空,用 task_message 兜底 if not messages: messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": task_message}, ] agent_id = int(task.get("agent_id", 0)) conversation_id = task.get("conversation_id") multi_agent_mode = bool(task.get("multi_agent_mode")) thinking_mode = task.get("thinking_mode") or "fast" model_key = task.get("model_key") display_name = task.get("display_name") role_id = task.get("role_id") multi_agent_state = None if multi_agent_mode and conversation_id: multi_agent_state = self.get_or_create_multi_agent_state(conversation_id) # 如果 snapshot 里没有该实例,根据 task_record 重建一个 if multi_agent_state and not multi_agent_state.get_instance(agent_id): from modules.multi_agent.state import AgentInstance inst = AgentInstance( agent_id=agent_id, role_id=role_id or "", display_name=display_name or f"Agent_{agent_id}", task_id=task_id, status=status if status in ("running", "idle") else "running", summary=task.get("summary", ""), ) try: multi_agent_state.register_instance(inst) except ValueError: pass sub_agent = SubAgentTask( manager=self, task_record=task, task_message=task_message, system_prompt=system_prompt, model_key=model_key, thinking_mode=thinking_mode, multi_agent_mode=multi_agent_mode, multi_agent_state=multi_agent_state, display_name=display_name, ) sub_agent.messages = messages # 重启后统一置为 idle,等待主智能体再次发消息才继续 if multi_agent_mode: sub_agent._idle = True task["status"] = "idle" task["updated_at"] = time.time() if multi_agent_state: multi_agent_state.mark_status(agent_id, "idle") # 同步落盘 output.json,保证前端状态一致 try: output_file = Path(task.get("output_file", "")) if output_file.exists(): output_data = json.loads(output_file.read_text(encoding="utf-8")) else: output_data = {} output_data["status"] = "idle" output_data["success"] = None output_file.parent.mkdir(parents=True, exist_ok=True) output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8") except Exception as exc: logger.warning(f"[restore] 更新任务 {task_id} output 文件失败: {exc}") task_coro = sub_agent.run() asyncio_task = self._run_coro(task_coro) sub_agent._task = asyncio_task self._running_tasks[task_id] = asyncio_task 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): try: self._running_tasks.pop(tid, None) self._sub_agent_instances.pop(aid, None) self.reconcile_task_states(conversation_id=conversation_id) if multi_agent_mode and state: 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) restored += 1 ma_debug( "manager_restore_sub_agent", task_id=task_id, agent_id=agent_id, display_name=display_name, multi_agent_mode=multi_agent_mode, status=status, message_count=len(messages), ) if restored: self._save_state() return restored # ------------------------------------------------------------------ # 多智能体模式:状态管理、外部接口、消息注入 # ------------------------------------------------------------------ def get_or_create_multi_agent_state(self, conversation_id: str): """获取或为该会话创建 MultiAgentState。""" from modules.multi_agent.state import MultiAgentState state = self.multi_agent_states.get(conversation_id) 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 state = MultiAgentState(conversation_id=conversation_id) 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 def get_multi_agent_state(self, conversation_id: str): """获取该会话的多智能体状态。""" 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: """删除会话状态(会话结束时调用)。""" 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() 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: """同事件循环中向子智能体上下文插入 user 消息。 适用于 ask_other_agent / send_message_to_sub_agent / answer_sub_agent_question_ (非阻塞到工具结果的路径)。返回 True 表示成功注入。 """ # 查找该 agent_id 对应的 running SubAgentTask 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: return False sub_agent.inject_message(message_text) return True def _find_sub_agent_task_by_agent_id(self, agent_id: int) -> Optional[Any]: """通过遍历创建中的 task 查找活 SubAgentTask 实例。 这是个 helper:在主实现中我们需要保留从 agent_id 到 SubAgentTask 的引用。 理论上可以在 create_sub_agent 时把 sub_agent 存起来,这里使用 rs safer贪心法: 遊历 _running_tasks 不为可行,因为 asyncio.Task 不抽不包含 SubAgentTask引用。 我们改为 `SubAgentTask` 对象列表供查询。 """ # 优先查缓存:create_sub_agent 时的字段 for inst in self._sub_agent_instances.values(): if inst.agent_id == agent_id: return inst return None def _on_multi_agent_task_done(self, task_id: str, agent_id: int, state: Any, sub_agent: Any) -> None: """SubAgentTask 结束回调会调这个更新 MultiAgentState 实例状态。""" final_task = self.tasks.get(task_id) or {} final_status_before = final_task.get("status") ma_debug( "manager_on_multi_agent_task_done", 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 设置) final_status = final_task.get("status") if final_status in TERMINAL_STATUSES: 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": state.mark_status(agent_id, "terminated") ma_debug("manager_ma_state_set", agent_id=agent_id, status="terminated", reason="task_terminated_status") else: 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: """将宿主机路径映射为容器内路径(仅用于提示展示)。""" if not self.container_session or getattr(self.container_session, "mode", None) != "docker": return str(host_path) mount_path = (getattr(self.container_session, "mount_path", None) or "/workspace").rstrip("/") or "/workspace" try: relative = host_path.resolve().relative_to(self.project_path) except Exception: return mount_path if str(relative) in {"", "."}: return mount_path return str(PurePosixPath(mount_path) / PurePosixPath(relative.as_posix()))