From 811974d6e71b132fffb1abaf24fc927a6ae98be8 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 12 Jul 2026 03:26:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(multi-agent):=20=E5=9C=A8=E7=8E=B0?= =?UTF-8?q?=E6=9C=89=E6=9E=B6=E6=9E=84=E4=B8=8A=E7=9B=B4=E6=8E=A5=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=A4=9A=E6=99=BA=E8=83=BD=E4=BD=93=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 放弃完全隔离策略,改为在现有 MainTerminal/SubAgentManager/SubAgentTask 主链路 按对话级开关 metadata.multi_agent_mode=true 增加多智能体分支。 新增模块: - modules/multi_agent/__init__.py: 模块入口 - modules/multi_agent/role_store.py: 角色 Markdown Frontmatter 解析与归档 - modules/multi_agent/state.py: 多智能体会话状态机与消息格式化 - modules/multi_agent/prompts.py: 主智能体(Team Leader) + 子智能体提示词 - modules/multi_agent/tools.py: 9 个主智能体工具 + 4 个子智能体工具定义 - server/multi_agent.py: /multiagent/new 页面 + /api/multiagent/* 蓝图 现有代码改动: - modules/sub_agent/task.py: 扩展 multi_agent_mode/multi_agent_state/display_name 字段, 增加 ask_master/ask_other_agent/answer_other_agent/list_active_sub_agents 工具处理逻辑, 子智能体自然结束 assistant 输出即本轮结束(不调用 finish_task),上下文保留。 - modules/sub_agent/manager.py: create_sub_agent 增加 multi_agent_mode/role_id/display_name 参数, 增加 get_or_create_multi_agent_state/get_multi_agent_state/inject_message_to_sub_agent/_on_multi_agent_task_done 方法。 - core/main_terminal_parts/tools_definition/agent_tools.py: 多智能体模式下用 modules.multi_agent.tools 替换旧版工具集。 - core/main_terminal_parts/context/messages.py: 多智能体模式下追加 Team Leader 系统提示词。 - core/main_terminal_parts/tools_execution.py: create_sub_agent handler 增加多智能体分支,新增 send_message_to_sub_agent/ask_sub_agent/answer_sub_agent_question/create_custom_agent/list_agents/list_active_sub_agents handler。 - core/web_terminal.py: load_conversation 时检测 metadata.multi_agent_mode 设置 self.multi_agent_mode。 - server/app_legacy.py: 注册 multi_agent_bp 蓝图。 前端改动: - static/src/auth/LoginApp.vue: 登录页增加'多智能体模式(beta)'按钮 - static/src/app/methods/ui/route.ts: 识别 /multiagent/new 和 /multiagent/conv_xxx 路径,进入多智能体模式并创建带 metadata.multi_agent_mode=true 的对话 - static/src/app/state.ts: 增加 multiAgentMode 状态字段 数据: - ~/.astrion/astrion/host/mutiagents/agents/: 4 个预置角色 ui-operator / full-stack-engineer / code-reviewer / researcher - ~/.astrion/astrion/host/mutiagents/conversations/: 会话数据 验证:所有 Python 文件语法检查通过;冒烟测试 test.test_server_refactor_smoke 6 项全通过;前端构建通过(6.04s);模块导入与功能断言测试全部通过。 --- core/main_terminal_parts/context/messages.py | 10 + .../tools_definition/agent_tools.py | 8 + core/main_terminal_parts/tools_execution.py | 275 +++++++++++--- core/web_terminal.py | 8 + modules/multi_agent/__init__.py | 53 +++ modules/multi_agent/prompts.py | 156 ++++++++ modules/multi_agent/role_store.py | 208 +++++++++++ modules/multi_agent/state.py | 339 ++++++++++++++++++ modules/multi_agent/tools.py | 323 +++++++++++++++++ modules/sub_agent/manager.py | 111 +++++- modules/sub_agent/task.py | 129 ++++++- server/app_legacy.py | 2 + server/multi_agent.py | 199 ++++++++++ static/src/app/methods/ui/route.ts | 82 +++++ static/src/app/state.ts | 2 + static/src/auth/LoginApp.vue | 13 + 16 files changed, 1866 insertions(+), 52 deletions(-) create mode 100644 modules/multi_agent/__init__.py create mode 100644 modules/multi_agent/prompts.py create mode 100644 modules/multi_agent/role_store.py create mode 100644 modules/multi_agent/state.py create mode 100644 modules/multi_agent/tools.py create mode 100644 server/multi_agent.py diff --git a/core/main_terminal_parts/context/messages.py b/core/main_terminal_parts/context/messages.py index 4306034..dfc9ea5 100644 --- a/core/main_terminal_parts/context/messages.py +++ b/core/main_terminal_parts/context/messages.py @@ -278,6 +278,16 @@ class MessagesMixin: "frozen_disabled_tools_prompt", lambda: self._format_disabled_tool_notice() or "", ) + + # 多智能体模式提示词:当会话 metadata.multi_agent_mode 为真时追加 + if getattr(self, "multi_agent_mode", False): + try: + from modules.multi_agent.prompts import build_multi_agent_master_prompt + multi_agent_prompt = build_multi_agent_master_prompt(self.project_path, base="") + if multi_agent_prompt: + messages.append({"role": "system", "content": multi_agent_prompt}) + except Exception as exc: + logger.warning(f"[messages] 注入多智能体 prompt 失败: {exc}") if disabled_notice: messages.append({ "role": "system", diff --git a/core/main_terminal_parts/tools_definition/agent_tools.py b/core/main_terminal_parts/tools_definition/agent_tools.py index 0c3189d..700bcf5 100644 --- a/core/main_terminal_parts/tools_definition/agent_tools.py +++ b/core/main_terminal_parts/tools_definition/agent_tools.py @@ -91,6 +91,14 @@ DISABLE_LENGTH_CHECK = True class ToolsDefinitionAgentToolsMixin: def _build_agent_tools(self) -> List[Dict]: + # 多智能体模式下,主智能体不再使用旧版的 4 个子智能体工具 + # 而是用 modules/multi_agent/tools.build_master_tools_for_conversation() 返回的新集 + if getattr(self, "multi_agent_mode", False): + try: + from modules.multi_agent.tools import build_master_tools_for_conversation + return build_master_tools_for_conversation() + except Exception as exc: + logger.warning(f"[tools] 加载多智能体工具失败,回退旧版: {exc}") return [ { "type": "function", diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index f80e16e..e6838bd 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -1728,66 +1728,245 @@ class MainTerminalToolsExecutionMixin: ) elif tool_name == "create_sub_agent": - result = self.sub_agent_manager.create_sub_agent( - agent_id=arguments.get("agent_id"), - summary=arguments.get("summary", ""), - task=arguments.get("task", ""), - deliverables_dir=arguments.get("deliverables_dir", ""), - run_in_background=arguments.get("run_in_background", False), - timeout_seconds=arguments.get("timeout_seconds"), - thinking_mode=arguments.get("thinking_mode"), - conversation_id=self.context_manager.current_conversation_id - ) + # 多智能体模式:create_sub_agent 走新签名,需要 role_id/display_name/multi_agent_mode + if getattr(self, "multi_agent_mode", False): + role_id = arguments.get("role_id") + if not role_id: + result = {"success": False, "error": "多智能体模式下 create_sub_agent 必须指定 role_id"} + else: + try: + from modules.multi_agent.role_store import load_preset_role + from modules.multi_agent.prompts import build_multi_agent_sub_agent_prompt + role = load_preset_role(role_id) + if not role: + result = {"success": False, "error": f"角色不存在: {role_id}"} + else: + conv_id = self.context_manager.current_conversation_id + multi_agent_state = self.sub_agent_manager.get_or_create_multi_agent_state(conv_id) + # 分配 agent_id:如果未传入则自动递增 + agent_id = arguments.get("agent_id") + if not agent_id: + agent_id = multi_agent_state.next_agent_id_for_role(role_id) + # 构造显示名 + display_name = role.display_name(int(agent_id)) + # 构造多智能体版系统提示词 + workspace_path = str(getattr(self, "project_path", "")) + system_prompt = build_multi_agent_sub_agent_prompt(role.body_prompt, display_name, workspace_path) + # 构造 task_message(作为 Team Leader 的任务发布) + from modules.multi_agent.state import build_master_dispatch_text + task_message = build_master_dispatch_text(arguments.get("task", "")) + # 加载并覆盖作业:multi_agent_mode/sub_agent_manager.create_sub_agent 的作业路径 + summary_text = (arguments.get("summary") or f"{role.name}作业")[:80] + deliverables_dir = arguments.get("deliverables_dir", f"sub_agent_results/agent_{agent_id}") + thinking_mode = arguments.get("thinking_mode") or role.thinking_mode or "fast" + # 走原行 发事件创建(避免后期重建提供重复工能重费,直接使用 multi_agent_mode=True 调用) + result = self.sub_agent_manager.create_sub_agent( + agent_id=int(agent_id), + summary=summary_text, + task=arguments.get("task", ""), + deliverables_dir=deliverables_dir, + run_in_background=bool(arguments.get("run_in_background", True)), + timeout_seconds=arguments.get("timeout_seconds"), + conversation_id=conv_id, + model_key=role.model_key, + thinking_mode=thinking_mode, + multi_agent_mode=True, + role_id=role_id, + display_name=display_name, + ) + # 在多智能体模式下,主进程 create_sub_agent 总是后台启动, + # 主智能体不需要阻塞等待,而是通过子智能体输出转发拿进度。 + except Exception as exc: + logger.exception("[multi_agent] create_sub_agent failed") + result = {"success": False, "error": str(exc)} + else: + result = self.sub_agent_manager.create_sub_agent( + agent_id=arguments.get("agent_id"), + summary=arguments.get("summary", ""), + task=arguments.get("task", ""), + deliverables_dir=arguments.get("deliverables_dir", ""), + run_in_background=arguments.get("run_in_background", False), + timeout_seconds=arguments.get("timeout_seconds"), + thinking_mode=arguments.get("thinking_mode"), + conversation_id=self.context_manager.current_conversation_id + ) - # 如果不是后台运行,阻塞等待完成 - if not arguments.get("run_in_background", False) and result.get("success"): - task_id = result.get("task_id") - wait_result = self.sub_agent_manager.wait_for_completion( - task_id=task_id, - timeout_seconds=arguments.get("timeout_seconds") - ) - # 合并结果:保留创建元数据,使用执行结果作为主体展示, - # 避免 wait_result 里的 success=False + 旧 message 导致 - # 「create_sub_agent 失败:子智能体 X 已创建」的误导性文案。 - creation_meta = { - "agent_id": result.get("agent_id"), - "task_id": result.get("task_id"), - "deliverables_dir": result.get("deliverables_dir"), - "run_in_background": False, - } - execution_message = ( - wait_result.get("message") - or wait_result.get("system_message") - or result.get("message") - ) - result = { - **result, - **wait_result, - **creation_meta, - "message": execution_message, - } - # 阻塞式执行不需要额外插入 system 消息 - result.pop("system_message", None) - # 标记已通知,避免后续轮询再插入 system 消息 - try: - task = self.sub_agent_manager.tasks.get(task_id) - if isinstance(task, dict): - task["notified"] = True - task["updated_at"] = time.time() - self.sub_agent_manager._save_state() - except Exception: - pass + # 如果不是后台运行,阻塞等待完成 + if not arguments.get("run_in_background", False) and result.get("success"): + task_id = result.get("task_id") + wait_result = self.sub_agent_manager.wait_for_completion( + task_id=task_id, + timeout_seconds=arguments.get("timeout_seconds") + ) + # 合并结果:保留创建元数据,使用执行结果作为主体展示, + # 避免 wait_result 里的 success=False + 旧 message 导致 + # 「create_sub_agent 失败:子智能体 X 已创建」的误导性文案。 + creation_meta = { + "agent_id": result.get("agent_id"), + "task_id": result.get("task_id"), + "deliverables_dir": result.get("deliverables_dir"), + "run_in_background": False, + } + execution_message = ( + wait_result.get("message") + or wait_result.get("system_message") + or result.get("message") + ) + result = { + **result, + **wait_result, + **creation_meta, + "message": execution_message, + } + # 阻塞式执行不需要额外插入 system 消息 + result.pop("system_message", None) + # 标记已通知,避免后续轮询再插入 system 消息 + try: + task = self.sub_agent_manager.tasks.get(task_id) + if isinstance(task, dict): + task["notified"] = True + task["updated_at"] = time.time() + self.sub_agent_manager._save_state() + except Exception: + pass elif tool_name == "terminate_sub_agent": result = self.sub_agent_manager.terminate_sub_agent( agent_id=arguments.get("agent_id") ) + # 多智能体模式:同步状态到 MultiAgentState + if getattr(self, "multi_agent_mode", False): + try: + conv_id = self.context_manager.current_conversation_id + state = self.sub_agent_manager.get_multi_agent_state(conv_id) + if state: + state.mark_status(int(arguments.get("agent_id")), "terminated") + except Exception: + pass elif tool_name == "get_sub_agent_status": result = self.sub_agent_manager.get_sub_agent_status( agent_ids=arguments.get("agent_ids", []) ) + # 多智能体模式专属工具:send_message_to_sub_agent / ask_sub_agent / answer_sub_agent_question / create_custom_agent / list_agents / list_active_sub_agents + elif tool_name == "send_message_to_sub_agent": + if not getattr(self, "multi_agent_mode", False): + result = {"success": False, "error": "该工具仅在多智能体模式下可用"} + else: + try: + from modules.multi_agent.state import build_master_message_to_sub_agent + agent_id = int(arguments.get("agent_id", 0)) + message = arguments.get("message", "") + conv_id = self.context_manager.current_conversation_id + state = self.sub_agent_manager.get_multi_agent_state(conv_id) + if not state: + result = {"success": False, "error": "多智能体状态未就绪"} + else: + # 构造消息文本并插入子对话 + text = build_master_message_to_sub_agent(message) + ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text) + if not ok: + result = {"success": False, "error": f"子智能体 {agent_id} 不存在或已结束"} + else: + result = {"success": True, "agent_id": agent_id} + except Exception as exc: + result = {"success": False, "error": str(exc)} + + elif tool_name == "ask_sub_agent": + if not getattr(self, "multi_agent_mode", False): + result = {"success": False, "error": "该工具仅在多智能体模式下可用"} + else: + try: + from modules.multi_agent.state import format_multi_agent_message, TYPE_ASK + agent_id = int(arguments.get("agent_id", 0)) + question = arguments.get("question", "") + timeout = int(arguments.get("timeout_seconds", 600)) + conv_id = self.context_manager.current_conversation_id + state = self.sub_agent_manager.get_multi_agent_state(conv_id) + if not state: + result = {"success": False, "error": "多智能体状态未就绪"} + else: + # 构造提问并插入子对话(子智能体下一轮 assistant 输出作为回答插入主对话) + text = format_multi_agent_message( + display_name="Team Leader", + msg_type=TYPE_ASK, + content=question, + target=state.get_instance(agent_id).display_name if state.get_instance(agent_id) else f"Agent_{agent_id}", + ) + ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text) + if not ok: + result = {"success": False, "error": f"子智能体 {agent_id} 不存在"} + else: + # 阻塞等待子智能体下一轮输出作为回答 + answer = await state.wait_for_answer( + question_id=f"ask_sub_agent_{int(time.time())}", + agent_id=agent_id, + timeout=timeout, + ) + result = {"success": True, "answer": answer} + except asyncio.TimeoutError: + result = {"success": False, "error": "等待子智能体回答超时"} + except Exception as exc: + result = {"success": False, "error": str(exc)} + + elif tool_name == "answer_sub_agent_question": + if not getattr(self, "multi_agent_mode", False): + result = {"success": False, "error": "该工具仅在多智能体模式下可用"} + else: + try: + question_id = arguments.get("question_id", "") + answer = arguments.get("answer", "") + conv_id = self.context_manager.current_conversation_id + state = self.sub_agent_manager.get_multi_agent_state(conv_id) + if not state: + result = {"success": False, "error": "多智能体状态未就绪"} + else: + ok = state.provide_answer(question_id, answer) + result = {"success": bool(ok), "question_id": question_id} + except Exception as exc: + result = {"success": False, "error": str(exc)} + + elif tool_name == "create_custom_agent": + try: + from modules.multi_agent.role_store import RoleConfig, save_custom_role, list_roles + role_id = arguments.get("role_id", "").strip() + name = arguments.get("name", "").strip() + body_prompt = arguments.get("body_prompt", "").strip() + description = arguments.get("description", "").strip() + thinking_mode_arg = arguments.get("thinking_mode", "fast") + if not role_id or not name or not body_prompt: + result = {"success": False, "error": "role_id/name/body_prompt 必填"} + else: + existing_ids = {r.role_id for r in list_roles()} + if role_id in existing_ids: + result = {"success": False, "error": f"角色 {role_id} 已存在"} + else: + role = RoleConfig(role_id=role_id, name=name, description=description, body_prompt=body_prompt, thinking_mode=thinking_mode_arg) + f = save_custom_role(role) + result = {"success": True, "role_id": role_id, "file": str(f)} + except Exception as exc: + result = {"success": False, "error": str(exc)} + + elif tool_name == "list_agents": + try: + from modules.multi_agent.role_store import list_roles + roles = list_roles() + result = {"success": True, "roles": [r.to_dict() for r in roles]} + except Exception as exc: + result = {"success": False, "error": str(exc)} + + elif tool_name == "list_active_sub_agents": + try: + conv_id = self.context_manager.current_conversation_id + state = self.sub_agent_manager.get_multi_agent_state(conv_id) + if not state: + result = {"success": True, "agents": []} + else: + result = {"success": True, "agents": [a.to_dict() for a in state.list_all()]} + except Exception as exc: + result = {"success": False, "error": str(exc)} + elif tool_name == "trigger_easter_egg": result = self.easter_egg_manager.trigger_effect(arguments.get("effect")) diff --git a/core/web_terminal.py b/core/web_terminal.py index dc24349..7f7cbbd 100644 --- a/core/web_terminal.py +++ b/core/web_terminal.py @@ -347,6 +347,14 @@ class WebTerminal(MainTerminal): self.pending_permission_mode = str(meta.get("pending_permission_mode") or "").strip().lower() or None self.pending_execution_mode = str(meta.get("pending_execution_mode") or "").strip().lower() or None self.pending_network_permission = str(meta.get("pending_network_permission") or "").strip().lower() or None + # 多智能体模式:以会话 metadata.multi_agent_mode 为唯一切换开关 + self.multi_agent_mode = bool(meta.get("multi_agent_mode", False)) + # 同步主智能体 sub_agent_manager 的 开关 + try: + if hasattr(self, "sub_agent_manager"): + self.sub_agent_manager.multi_agent_mode = self.multi_agent_mode + except Exception: + pass except Exception: pass # 重置相关状态 diff --git a/modules/multi_agent/__init__.py b/modules/multi_agent/__init__.py new file mode 100644 index 0000000..d6adab6 --- /dev/null +++ b/modules/multi_agent/__init__.py @@ -0,0 +1,53 @@ +"""多智能体模式核心实现。 + +由 conversations metadata.multi_agent_mode = true 触发启用, +不另起隔离链路,直接复用现有 MainTerminal、SubAgentManager、SubAgentTask +但通过对话级开关注入多智能体版工具集、prompt、消息路由。 + +关键组件: +- RoleConfig:角色 Markdown Frontmatter 解析与归档 +- MultiAgentState:一个多智能体会话的运行态状态机 +- 消息格式构造:format_multi_agent_message +- 工具定义:master 侧、sub_agent 侧扩展 +- prompt 构造:build_multi_agent_master_prompt / build_multi_agent_sub_agent_prompt +""" +from __future__ import annotations + +from .state import MultiAgentState, format_multi_agent_message +from .role_store import ( + RoleConfig, + load_preset_role, + list_roles, + save_custom_role, + build_role_system_prompt, +) +from .prompts import ( + build_multi_agent_master_prompt, + build_multi_agent_sub_agent_prompt, + MULTI_AGENT_MASTER_PROMPT_BODY, + MULTI_AGENT_SUB_AGENT_PROMPT_BODY, +) +from .tools import ( + MULTI_AGENT_MASTER_TOOLS, + MULTI_AGENT_SUB_AGENT_TOOLS, + build_master_tools_for_conversation, + build_sub_agent_tools_for_role, +) + +__all__ = [ + "MultiAgentState", + "format_multi_agent_message", + "RoleConfig", + "load_preset_role", + "list_roles", + "save_custom_role", + "build_role_system_prompt", + "build_multi_agent_master_prompt", + "build_multi_agent_sub_agent_prompt", + "MULTI_AGENT_MASTER_PROMPT_BODY", + "MULTI_AGENT_SUB_AGENT_PROMPT_BODY", + "MULTI_AGENT_MASTER_TOOLS", + "MULTI_AGENT_SUB_AGENT_TOOLS", + "build_master_tools_for_conversation", + "build_sub_agent_tools_for_role", +] \ No newline at end of file diff --git a/modules/multi_agent/prompts.py b/modules/multi_agent/prompts.py new file mode 100644 index 0000000..44256a8 --- /dev/null +++ b/modules/multi_agent/prompts.py @@ -0,0 +1,156 @@ +"""多智能体模式的系统提示词。""" +from __future__ import annotations + +from typing import Optional + + +MULTI_AGENT_MASTER_PROMPT_BODY = """# 多智能体模式 + +你是 **Team Leader**(团队领导者),负责协调多个子智能体分工协作完成用户的复杂任务。 + +## 工作原则 + +- **主动分工**:除非任务极其简单或明确不需要子智能体,否则主动把任务拆解并指派给合适的角色。 +- **明确指令**:用 `send_message_to_sub_agent` 发任务时,写清楚任务目标、范围、产出要求。 +- **及时回答**:当子智能体通过 `ask_master` 提问时,必须尽快通过 `answer_sub_agent_question` 回答。 +- **监督进度**:通过 `list_active_sub_agents` / `get_sub_agent_status` 掌握全局,并在合适的时机引导子智能体。 +- **运行时引导**:看到子智能体作出的步骤需要纠正时,立刻用 `send_message_to_sub_agent` 在其运行期间插入消息干预。 +- **明确问答**:当你需要一个具体的、可被回答的小问题被某个子智能体处理时,用 `ask_sub_agent` 阻塞等待一轮回答。 + +## 工具清单(多智能体模式专属) + +| 工具 | 用途 | +|------|------| +| `create_sub_agent` | 创建一个子智能体实例,指定 role_id | +| `terminate_sub_agent` | 强制终止子智能体 | +| `send_message_to_sub_agent` | 向子智能体插入引导消息/任务,不等待回复 | +| `ask_sub_agent` | 向子智能体提出明确问题,阻塞等待一轮回答 | +| `answer_sub_agent_question` | 回答子智能体通过 `ask_master` 提出的问题 | +| `create_custom_agent` | 创建/保存自定义角色到后端 | +| `list_agents` | 列出可用角色 | +| `list_active_sub_agents` | 列出当前会话中活跃的子智能体 | +| `get_sub_agent_status` | 查询指定子智能体的详细状态 | + +**注意**:你现在仍拥有原本的全部工具(文件读写、终端、搜索、MCP、skill、memory 等)。以上只列出多智能体模式新增的工具——它们**替换**了原有的 `create_sub_agent` / `close_sub_agent` / `get_sub_agent_status`,使用语义有变化。 + +## 你会收到的消息格式 + +子智能体输出(每轮 assistant 文字输出都会通过 user 消息插到你的对话里): + +``` +来自 UI Operator_1 的任务进度输出 +id: out_xxxxxxxx + + + +我现在开始分析现有设计风格... + + +``` + +子智能体向你提问: + +``` +来自 Full-Stack Engineer_1 的提问 +id: ask_fse_001 + + + +我应该使用 JWT 还是 Session Cookie? + + +``` + +**回答提问**必须用 `answer_sub_agent_question` 工具,传入 `question_id`(即消息里的 id)和 answer 文本。回答不会以 user 消息插入,而是直接返回到子智能体的 `ask_master` 工具结果中。 + +## 关于显示名 + +- 主智能体固定显示名:`Team Leader` +- 子智能体显示名:`{角色名}_{agent_id}`,如 `UI Operator_1`、`Full-Stack Engineer_2` +- 一个角色可以有多个实例(同 role_id 多 agent_id) + +## 关于通信协议的三条硬性原则 + +1. **接收方决定插入方式**:子智能体收到消息后,由它自己的状态决定是 inline 穿插还是开启新轮任务。你不要操心插入位置,只负责发起。 +2. **回答走工具结果而非 user 消息**:你回答子智能体提问用的是 `answer_sub_agent_question`,回答内容是工具结果,不需要写出 XML 包裹。 +3. **任务发布/消息/引导都走自然 XML 格式**:调用 `send_message_to_sub_agent` 时只写正文,后端会自动包成 `来自 Team Leader 的消息 / 任务发布` 格式插入子对话。 + +## 关于团队全局可见 + +子智能体之间通过 `ask_other_agent` / `answer_other_agent` 直接通信,会并行在自己的对话内进行。你只需要在 prompt 里要求子智能体「如要向其他子智能体提问,必须同时直接给你输出一条汇报」,这样你能掌握全局。但你**不需要**手动转发它们之间的问答。 +""" + + +MULTI_AGENT_SUB_AGENT_PROMPT_BODY = """# 多智能体身份 + +你是智能体集群团队的一员。你的团队通过分工协作完成复杂任务,主智能体 **Team Leader** 负责督导全局。 + +# 在任务中 + +- 不要频繁输出内容,不重要的内容会污染主智能体上下文 +- 只汇报关键步骤 +- 任务完成后给出详细结论 +- 自然结束输出即本轮任务结束;上下文会被保留,Team Leader 可能会再次发消息让你继续 + +# 沟通工具 + +- **需要 Team Leader 决策时**:调用 `ask_master` 工具,传入 question 文本 + - 工具会阻塞等待 Team Leader 通过 `answer_sub_agent_question` 给出回答 + - 你的 question 会以 XML 「提问」格式被插入主对话 +- **要问其他子智能体时**:调用 `ask_other_agent`,传入 target_agent_id 与 question + - 等待对方调用 `answer_other_agent` 回答 +- **要回答其他子智能体的提问时**:调用 `answer_other_agent`,传入 source_agent_id 与 question_id 和 answer + - 你的回答直接作为对方 `ask_other_agent` 工具的结果返回(不会以 user 消息插入对话) +- **查询当前活跃子智能体**:调用 `list_active_sub_agents` + +# 关于向你团队「汇报」的强制要求 + +**如果你要向其他子智能体提问,必须同时直接输出一条汇报给 Team Leader**(在你的普通文本输出里),说明: +1. 你为什么要问这个问题 +2. 你问了谁 +3. 你期望得到什么 + +不能偷偷沟通,Team Leader 需要看到完整协作流程。 + +# 输出格式 + +你每轮的普通 assistant 文字输出都会被自动捕获并以如下格式插入到主对话: + +``` +来自 {你的显示名} 的任务进度输出 +id: out_xxxxxxxx + +<{你的显示名}> + +{你的输出} + + +``` + +你不需要自己包裹 XML,直接输出正文即可。 +""" + + +def build_multi_agent_master_prompt(workspace_path: str, base: str = "") -> str: + """构造主智能体(Team Leader)的系统提示词。 + + `base` 一般为现有 MainTerminal 的 base 提示词(环境/工具概览等), + 我们在末尾追加多智能体模式专属正文。 + """ + if base and base.strip(): + return f"{base.rstrip()}\n\n{MULTI_AGENT_MASTER_PROMPT_BODY}\n" + return f"{MULTI_AGENT_MASTER_PROMPT_BODY}\n" + + +def build_multi_agent_sub_agent_prompt(role_body: str, display_name: str, workspace_path: str) -> str: + """构造子智能体的系统提示词。 + + `role_body` 为该角色 Markdown 文件 frontmatter 之后的自定义 prompt。 + `display_name` 为该实例的显示名(如 `UI Operator_1`)。 + """ + header = MULTI_AGENT_SUB_AGENT_PROMPT_BODY.rstrip() + return ( + f"{header}\n\n" + f"# 你的显示名\n\n你的显示名是 `{display_name}`。\n\n" + f"# 你的专属设定\n\n{role_body.strip()}\n" + ) \ No newline at end of file diff --git a/modules/multi_agent/role_store.py b/modules/multi_agent/role_store.py new file mode 100644 index 0000000..4e027d4 --- /dev/null +++ b/modules/multi_agent/role_store.py @@ -0,0 +1,208 @@ +"""多智能体角色存储与解析。 + +角色定义文件格式(Markdown + Frontmatter): + + --- + id: ui-operator + name: UI Operator + description: 界面设计与前端实现 + model: "" + thinking_mode: fast + skills: + - frontend-design + --- + + 自定义 prompt body... + +角色目录: +- ~/.astrion/astrion/host/mutiagents/agents/.md + +存储约定见 .astrion/memory/multi_agent_mode_design.md。 +""" +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any, Dict, List, Optional + +try: + from config.paths import IS_HOST_MODE, RUNTIME_ROOT +except ImportError: + # 防止直接 import 时 config 尚未加载,提供回退值 + IS_HOST_MODE = True + RUNTIME_ROOT = str(Path.home() / ".astrion" / "astrion") + +# 复用项目的 MUTIAGENTS 数据目录(保留原拼写 "mutiagents",见项目记忆) +DEFAULT_MUTIAGENTS_DIR = Path(RUNTIME_ROOT) / ("host" if IS_HOST_MODE else "web") / "mutiagents" +AGENTS_DIR_NAME = "agents" + +FRONTMATTER_RE = re.compile(r"^---\s*\n(?P.*?)\n---\s*\n(?P.*)$", re.S) + + +def _agents_dir() -> Path: + """返回角色根目录(按运行模式自动选择 host/web)。""" + base = Path(DEFAULT_MUTIAGENTS_DIR) + out = base / AGENTS_DIR_NAME + out.mkdir(parents=True, exist_ok=True) + return out + + +def _parse_frontmatter(text: str) -> tuple[Dict[str, Any], str]: + """解析 Markdown frontmatter,返回 (元数据 dict, body)。""" + m = FRONTMATTER_RE.match(text) + if not m: + return {}, text + raw_meta = m.group("body") or "" + body = m.group("rest") or "" + meta: Dict[str, Any] = {} + # 简易解析(k: v 与 k: 列表项,不引入 yaml 依赖) + current_key: Optional[str] = None + for line in raw_meta.splitlines(): + if not line.strip(): + continue + if line.startswith(" - ") or line.startswith("- "): + value = line.lstrip(" ").lstrip("- ").strip() + if current_key and isinstance(meta.get(current_key), list): + meta[current_key].append(value) + continue + if ":" in line: + k, v = line.split(":", 1) + k = k.strip() + v = v.strip().strip('"').strip("'") + if v: + meta[k] = v + current_key = None + else: + # 可能是列表块 + meta[k] = [] + current_key = k + return meta, body.strip() + + +def load_role_from_file(file_path: Path) -> Optional["RoleConfig"]: + """从 Markdown 文件加载角色定义。""" + if not file_path.exists() or not file_path.is_file(): + return None + text = file_path.read_text(encoding="utf-8") + meta, body = _parse_frontmatter(text) + if not meta.get("id") or not meta.get("name"): + return None + return RoleConfig( + role_id=str(meta["id"]), + name=str(meta["name"]), + description=str(meta.get("description") or ""), + body_prompt=body, + model_key=(str(meta["model"]) if meta.get("model") else None) or None, + thinking_mode=str(meta.get("thinking_mode") or "fast"), + skills=list(meta.get("skills") or []), + source_file=str(file_path), + ) + + +def load_preset_role(role_id: str) -> Optional["RoleConfig"]: + """从预设/自定义角色目录加载一个角色。""" + f = _agents_dir() / f"{role_id}.md" + return load_role_from_file(f) + + +def list_roles() -> List["RoleConfig"]: + """列出全部角色(预设 + 用户自定义)。""" + agents_dir = _agents_dir() + if not agents_dir.exists(): + return [] + roles: List[RoleConfig] = [] + for p in sorted(agents_dir.glob("*.md")): + r = load_role_from_file(p) + if r and r.role_id: + roles.append(r) + return roles + + +def save_custom_role(role: "RoleConfig") -> Path: + """把自定义角色保存到 agents 目录。""" + f = _agents_dir() / f"{role.role_id}.md" + f.parent.mkdir(parents=True, exist_ok=True) + f.write_text(_serialize_role(role), encoding="utf-8") + return f + + +def _serialize_role(role: "RoleConfig") -> str: + """把 RoleConfig 序列化为 Markdown frontmatter 字符串。""" + lines = ["---"] + lines.append(f"id: {role.role_id}") + lines.append(f"name: {role.name}") + if role.description: + # 单行描述转义 + desc = role.description.replace('"', '\\"') + lines.append(f'description: "{desc}"') + if role.model_key: + lines.append(f"model: {role.model_key}") + lines.append(f"thinking_mode: {role.thinking_mode or 'fast'}") + if role.skills: + lines.append("skills:") + for s in role.skills: + lines.append(f" - {s}") + lines.append("---") + lines.append("") + lines.append(role.body_prompt or "") + return "\n".join(lines) + + +def build_role_system_prompt(role: "RoleConfig") -> str: + """生成子智能体的「专属 prompt」部分(frontmatter 之后的 body)。""" + return role.body_prompt or "" + + +class RoleConfig: + """一个多智能体角色配置。""" + + __slots__ = ( + "role_id", + "name", + "description", + "body_prompt", + "model_key", + "thinking_mode", + "skills", + "source_file", + ) + + def __init__( + self, + *, + role_id: str, + name: str, + description: str = "", + body_prompt: str = "", + model_key: Optional[str] = None, + thinking_mode: str = "fast", + skills: Optional[List[str]] = None, + source_file: str = "", + ): + self.role_id = role_id + self.name = name + self.description = description + self.body_prompt = body_prompt + self.model_key = model_key + self.thinking_mode = thinking_mode or "fast" + self.skills = list(skills or []) + self.source_file = source_file + + def display_name(self, agent_id: int) -> str: + """根据实例 id 生成显示名。""" + return f"{self.name}_{agent_id}" + + def to_dict(self) -> Dict[str, Any]: + return { + "role_id": self.role_id, + "name": self.name, + "description": self.description, + "model_key": self.model_key, + "thinking_mode": self.thinking_mode, + "skills": self.skills, + "source_file": self.source_file, + } + + def __repr__(self) -> str: + return f"" \ No newline at end of file diff --git a/modules/multi_agent/state.py b/modules/multi_agent/state.py new file mode 100644 index 0000000..ddfdece --- /dev/null +++ b/modules/multi_agent/state.py @@ -0,0 +1,339 @@ +"""多智能体会话状态机。 + +一个 MultiAgentState 绑定到一个多智能体对话的 conversation_id,维护: +- 已创建的子智能体实例(agent_id ↔ role_id ↔ display_name ↔ task_id ↔ status) +- 待插入到主对话的待发 user 消息队列(pending_master_messages) +- 主智能体工具调用 answer_sub_agent_question / answer_other_agent 写回答案的 futomap +- 子智能体调用 ask_master / ask_other_agent 时挂起的 futomap + +关键约定(来自 .astrion/memory/multi_agent_mode_design.md): +- 消息格式:`来自 {显示名} 的{类型}\\nid: {消息id}\\n\\n<{显示名}>\\n<{标签}>\\n{内容}\\n\\n` +- 接收方决定插入方式: + - 子智能体 ask 阻塞等待 → main 调 answer_* 返回到工具结果 + - 子智能体 idle 状态 → 主对话的 pending_master_messages 直接插入新轮 user 消息 + - 子智能体 running 中 → inline 插入到当前末尾(在下一轮 model 调用前合并 messages) +- 通信是「工具调用提问」+「回答返回到工具结果」;其他场景(输出/进度/完成/任务发布/消息/回答) + 才以 user 消息格式插入对话。 +""" +from __future__ import annotations + +import asyncio +import json +import uuid +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from modules.sub_agent.task import SubAgentTask + +# ---------- 消息类型常量 ---------- +TYPE_TASK = "Task" # 主→子 任务发布 +TYPE_OUTPUT = "Output" # 子→主 进度/完成输出(统一) +TYPE_ASK = "Ask" # 子→主 / 子→子 提问 +TYPE_ANSWER = "Answer" # 主→子 / 子→子 回答(不插入对话,仅做工具结果) +TYPE_MESSAGE = "Message" # 任意方向 消息 +# 内部枚举到此 + +QUESTION_PREFIX_ASK_MASTER = "ask_master" +QUESTION_PREFIX_ASK_OTHER = "ask_other" + + +def format_multi_agent_message( + *, + display_name: str, + msg_type: str, + content: str, + msg_id: Optional[str] = None, + target: Optional[str] = None, + extra_attrs: Optional[Dict[str, str]] = None, +) -> str: + """按统一格式构造 user 消息字符串。 + + Args: + display_name: 发出方显示名(如 UI Operator_1 / Team Leader) + msg_type: 消息类型,对应上方 TYPE_* 常量 + content: 消息正文 + msg_id: 消息 id;不传则自动生成 + target: 接收方显示名(用于子→子 提问时标明对谁提问) + extra_attrs: 额外标签属性(如 question_id="ask_xxx") + """ + if not msg_id: + msg_id = f"msg_{uuid.uuid4().hex[:10]}" + + # 第一行:自然语言前缀(含 target 标识) + if target: + prefix = f"来自 {display_name} 向 {target} 的{msg_type_to_text(msg_type)}" + else: + prefix = f"来自 {display_name} 的{msg_type_to_text(msg_type)}" + + # 第二行:id + id_line = f"id: {msg_id}" + + # 属性 attr 字符串 + attrs = "" + if target: + attrs += f' target="{target}"' + if extra_attrs: + for k, v in extra_attrs.items(): + attrs += f' {k}="{v}"' + + # XML 包裹 + tag = msg_type + xml = ( + f"<{display_name}>\n" + f"<{tag}{attrs}>\n" + f"{content}\n" + f"\n" + f"" + ) + + return f"{prefix}\n{id_line}\n\n{xml}" + + +def msg_type_to_text(msg_type: str) -> str: + """把 TYPE_* 转为中文短语,用于 prompt 前缀。""" + mapping = { + TYPE_TASK: "任务发布", + TYPE_OUTPUT: "任务进度输出", + TYPE_ASK: "提问", + TYPE_ANSWER: "回答", + TYPE_MESSAGE: "消息", + } + return mapping.get(msg_type, msg_type) + + +def build_master_dispatch_text(task: str, msg_id: Optional[str] = None) -> str: + """主智能体发布任务时插入到子智能体对话的 user 消息文本。""" + return format_multi_agent_message( + display_name="Team Leader", + msg_type=TYPE_TASK, + content=task, + msg_id=msg_id, + ) + + +def build_sub_agent_output_text(display_name: str, content: str, msg_id: Optional[str] = None) -> str: + """子智能体输出(进度或完成)插入到主对话的 user 消息文本。""" + return format_multi_agent_message( + display_name=display_name, + msg_type=TYPE_OUTPUT, + content=content, + msg_id=msg_id, + ) + + +def build_sub_agent_ask_master_text(display_name: str, question: str, question_id: str) -> str: + """子智能体向主智能体提问时插入到主对话的 user 消息文本。""" + return format_multi_agent_message( + display_name=display_name, + msg_type=TYPE_ASK, + content=question, + msg_id=question_id, + ) + + +def build_sub_agent_ask_other_text( + display_name: str, + target_display: str, + question: str, + question_id: str, +) -> str: + """子智能体向另一个子智能体提问时插入到目标子智能体对话的文本。""" + return format_multi_agent_message( + display_name=display_name, + msg_type=TYPE_ASK, + content=question, + msg_id=question_id, + target=target_display, + ) + + +def build_master_message_to_sub_agent(message: str, msg_id: Optional[str] = None) -> str: + """主智能体 send_message_to_sub_agent 时插入子对话的 user 消息文本。""" + return format_multi_agent_message( + display_name="Team Leader", + msg_type=TYPE_MESSAGE, + content=message, + msg_id=msg_id, + ) + + +def build_master_answer_to_sub_agent( + display_name: str, + target_display: str, + answer: str, + question_id: str, +) -> str: + """主智能体回答插入到子对话(仅当子智能体 not waiting 或 idle 时走 user 消息路径)。""" + return format_multi_agent_message( + display_name=display_name, + msg_type=TYPE_ANSWER, + content=answer, + msg_id=question_id, + target=target_display, + extra_attrs={"question_id": question_id}, + ) + + +# ---------- 运行态状态机 ---------- +@dataclass +class AgentInstance: + """一个多智能体会话中已创建的子智能体实例。""" + + agent_id: int + role_id: str + display_name: str + task_id: str + status: str = "running" # running / idle / terminated / failed / timeout + summary: str = "" + created_at: float = field(default_factory=lambda: datetime.now().timestamp()) + last_output: str = "" + + def to_dict(self) -> Dict[str, Any]: + return { + "agent_id": self.agent_id, + "role_id": self.role_id, + "display_name": self.display_name, + "task_id": self.task_id, + "status": self.status, + "summary": self.summary, + "created_at": self.created_at, + "last_output": self.last_output, + } + + +class MultiAgentState: + """绑到一个 conversation_id 的多智能体运行态。 + + 线程安全:所有 pubic 方法均假设在 SubAgentManager 的事件循环线程中调用, + 或者由 chat task 主线程通过 manager 的 _run_coro 进入此循环。 + 跨线程访问通过 manager._run_coro 桥接,避免直接调用。 + """ + + def __init__(self, conversation_id: str): + self.conversation_id = conversation_id + # agent_id 映射;同一会话里 agent_id 唯一 + self.agents: Dict[int, AgentInstance] = {} + # task_id -> agent_id(便于在 SubAgentTask 完成时回写) + self.task_id_to_agent_id: Dict[str, int] = {} + # 主智能体待插入消息队列(每条都是字符串,由 chat task 取走) + self.pending_master_messages: List[str] = [] + # ask_master / ask_other_agent 的等待 future + # key = question_id, value = asyncio.Future (结果为 answer str 或 Exception) + self.pending_questions: Dict[str, asyncio.Future] = {} + # 一个 agent 可能同时只阻塞在一个 ask 工具上(最简实现) + # key = agent_id, value = question_id(表示当前 agent 正阻塞等待) + self.agent_blocking_question: Dict[int, str] = {} + # 角色实例计数:role_id -> 已分配的最大 agent_id(数字) + # 用于创建新实例时自动递增编号,但允许调用方显式指定 + self.role_counters: Dict[str, int] = {} + + # ----- 创建/查询 ----- + def next_agent_id_for_role(self, role_id: str) -> int: + """为指定角色分配下一个 agent_id 编号。""" + n = self.role_counters.get(role_id, 0) + 1 + self.role_counters[role_id] = n + return n + + def register_instance(self, instance: AgentInstance) -> None: + if instance.agent_id in self.agents: + raise ValueError(f"agent_id {instance.agent_id} 已存在") + self.agents[instance.agent_id] = instance + self.task_id_to_agent_id[instance.task_id] = instance.agent_id + + def get_instance(self, agent_id: int) -> Optional[AgentInstance]: + return self.agents.get(agent_id) + + def get_instance_by_task_id(self, task_id: str) -> Optional[AgentInstance]: + aid = self.task_id_to_agent_id.get(task_id) + if aid is None: + return None + return self.agents.get(aid) + + def list_active(self) -> List[AgentInstance]: + return [a for a in self.agents.values() if a.status == "running" or a.status == "idle"] + + def list_all(self) -> List[AgentInstance]: + return list(self.agents.values()) + + def mark_status(self, agent_id: int, status: str, last_output: str = "") -> None: + a = self.agents.get(agent_id) + if a: + a.status = status + if last_output: + a.last_output = last_output + + # ----- 主对话注入 ----- + def push_master_message(self, message_text: str) -> None: + """把一条 user 消息追加到主对话待插入队列。""" + self.pending_master_messages.append(message_text) + + def drain_master_messages(self) -> List[str]: + """取出(清空)所有待插入主对话的消息。""" + msgs = self.pending_master_messages + self.pending_master_messages = [] + return msgs + + def has_pending_master_messages(self) -> bool: + return len(self.pending_master_messages) > 0 + + # ----- 阻塞问答 ----- + async def wait_for_answer(self, question_id: str, agent_id: int, timeout: float = 600.0) -> str: + """子智能体 ask_* 工具调用后阻塞等待答案。 + + 返回 answer 字符串;超时/取消抛 asyncio.TimeoutError 或 CancelledError。 + """ + if question_id in self.pending_questions: + raise RuntimeError(f"question_id 已存在: {question_id}") + fut: asyncio.Future = asyncio.get_event_loop().create_future() + self.pending_questions[question_id] = fut + self.agent_blocking_question[agent_id] = question_id + try: + return await asyncio.wait_for(fut, timeout=timeout) + except asyncio.TimeoutError: + raise + finally: + self.pending_questions.pop(question_id, None) + if self.agent_blocking_question.get(agent_id) == question_id: + self.agent_blocking_question.pop(agent_id, None) + + def provide_answer(self, question_id: str, answer: str) -> bool: + """主/其他子智能体 answer_* 工具调用时回写答案。 + + 返回 True 表示找到等待中的 future;False 表示无等待方或已超时。 + """ + fut = self.pending_questions.get(question_id) + if not fut or fut.done(): + return False + try: + fut.set_result(answer) + except asyncio.InvalidStateError: + return False + return True + + def is_agent_blocking(self, agent_id: int) -> bool: + return agent_id in self.agent_blocking_question + + def get_blocking_question_id(self, agent_id: int) -> Optional[str]: + return self.agent_blocking_question.get(agent_id) + + # ----- 持久化(最简版) ----- + def to_snapshot(self) -> Dict[str, Any]: + return { + "conversation_id": self.conversation_id, + "agents": [a.to_dict() for a in self.agents.values()], + "role_counters": self.role_counters, + } + + @classmethod + def from_snapshot(cls, snapshot: Dict[str, Any]) -> "MultiAgentState": + state = cls(conversation_id=snapshot.get("conversation_id", "")) + state.role_counters = dict(snapshot.get("role_counters") or {}) + for a_data in snapshot.get("agents") or []: + a = AgentInstance(**a_data) + state.agents[a.agent_id] = a + if a.task_id: + state.task_id_to_agent_id[a.task_id] = a.agent_id + return state \ No newline at end of file diff --git a/modules/multi_agent/tools.py b/modules/multi_agent/tools.py new file mode 100644 index 0000000..741259a --- /dev/null +++ b/modules/multi_agent/tools.py @@ -0,0 +1,323 @@ +"""多智能体工具定义(OpenAI Function Calling 格式)。 + +主智能体侧(master):替换原有的 4 个旧版子智能体工具,新增 9 个多智能体工具。 +子智能体侧(sub_agent):在现有 8 个基础工具之外新增 4 个通信工具。 + +工具处理函数(handler)不在这里实现,而是在 SubAgentManager/SubAgentTask 中 +注册回调,工具执行入口仍走 SubAgentManager.execute_tool_for_sub_agent。 +""" +from __future__ import annotations + +from typing import Any, Dict, List, Optional + + +def _inject_intent(properties: Dict[str, Any]) -> Dict[str, Any]: + """为所有 properties 注入 intent 字段(与现有工具规范保持一致)。""" + out: Dict[str, Any] = {} + for k, v in properties.items(): + if k == "intent": + out[k] = v + continue + out[k] = v + # 在末尾追加 intent 字段 + if "intent" not in out: + out["intent"] = { + "type": "string", + "description": "用不超过15个字向用户说明你要做什么,例如:派遣UI Operator设计配色。", + } + return out + + +# ----- 主智能体工具 ----- +def _master_tool_create_sub_agent() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "create_sub_agent", + "description": ( + "创建一个属于多智能体团队的子智能体实例并启动。必须指定 role_id。" + "实例会注入该角色的专属 prompt 与多智能体协作工具(ask_master 等)。" + ), + "parameters": { + "type": "object", + "properties": _inject_intent({ + "role_id": { + "type": "string", + "description": "角色标识,例如 'ui-operator'/'full-stack-engineer'/'code-reviewer'/'researcher'。先用 list_agents 查看可用角色。", + }, + "task": { + "type": "string", + "description": "要交给该子智能体执行的任务描述。要求包含:目标、范围、产出、注意事项。", + }, + "agent_id": { + "type": "integer", + "description": "(可选)手动指定实例编号;不传时自动递增。", + }, + "deliverables_dir": { + "type": "string", + "description": "(可选)交付目录相对路径,留空则用 sub_agent_results/agent_{N}。", + }, + "timeout_seconds": {"type": "integer", "description": "超时秒数,默认 600。"}, + "thinking_mode": { + "type": "string", + "enum": ["fast", "thinking"], + "description": "(可选)覆盖角色默认思考模式。不填使用角色配置。", + }, + }), + "required": ["role_id", "task", "thinking_mode"], + }, + }, + } + + +def _master_tool_terminate_sub_agent() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "terminate_sub_agent", + "description": "强制终止指定子智能体实例。终止后无法恢复,已生成的文件保留。", + "parameters": { + "type": "object", + "properties": _inject_intent({ + "agent_id": {"type": "integer", "description": "要终止的子智能体编号。"}, + }), + "required": ["agent_id"], + }, + }, + } + + +def _master_tool_send_message_to_sub_agent() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "send_message_to_sub_agent", + "description": ( + "运行时引导/干预:向指定子智能体插入一条引导消息或新任务,立刻返回不等待回复。" + "用于看到子智能体中间输出后立即纠正方向、追加要求。消息接收方根据当前状态" + "(running/idle)自行决定是 inline 插入还是触发新一轮任务。" + ), + "parameters": { + "type": "object", + "properties": _inject_intent({ + "agent_id": {"type": "integer", "description": "目标子智能体编号。"}, + "message": {"type": "string", "description": "要插入的消息或新任务正文。"}, + }), + "required": ["agent_id", "message"], + }, + }, + } + + +def _master_tool_ask_sub_agent() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "ask_sub_agent", + "description": ( + "向指定子智能体提出一个明确问题并阻塞等待一轮回答(不是发起任务,是问问题)。" + "问题会以 `来自 Team Leader 的提问` 格式插入子对话,子智能体下一轮 assistant 输出" + "作为回答返回到此工具结果中。" + ), + "parameters": { + "type": "object", + "properties": _inject_intent({ + "agent_id": {"type": "integer", "description": "目标子智能体编号。"}, + "question": {"type": "string", "description": "要询问的问题,应简短明确。"}, + "timeout_seconds": {"type": "integer", "description": "等待回答超时秒数,默认 600。"}, + }), + "required": ["agent_id", "question"], + }, + }, + } + + +def _master_tool_answer_sub_agent_question() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "answer_sub_agent_question", + "description": ( + "回答子智能体通过 ask_master 工具提出的问题。回答内容会直接返回到子智能体" + "ask_master 工具的 tool_call 结果里(不会以 user 消息插入子对话)。" + ), + "parameters": { + "type": "object", + "properties": _inject_intent({ + "question_id": {"type": "string", "description": "提问消息里给出的 id,如 ask_xxx。"}, + "answer": {"type": "string", "description": "给子智能体的回答正文。"}, + }), + "required": ["question_id", "answer"], + }, + }, + } + + +def _master_tool_create_custom_agent() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "create_custom_agent", + "description": "创建/保存一个自定义角色到后端(~/.astrion/astrion/host/mutiagents/agents/)。后续可用 create_sub_agent 指定该角色。", + "parameters": { + "type": "object", + "properties": _inject_intent({ + "role_id": {"type": "string", "description": "角色标识(英文小写下划线,如 api-designer)。"}, + "name": {"type": "string", "description": "显示名(如 API Designer)。"}, + "description": {"type": "string", "description": "一句话简述职责。"}, + "body_prompt": {"type": "string", "description": "角色的自定义 prompt body(Markdown)。"}, + "thinking_mode": {"type": "string", "enum": ["fast", "thinking"], "description": "默认思考模式。"}, + }), + "required": ["role_id", "name", "body_prompt"], + }, + }, + } + + +def _master_tool_list_agents() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "list_agents", + "description": "列出所有可用的预置/自定义角色(role_id / name / 描述)。用于在 create_sub_agent 前选角色。", + "parameters": {"type": "object", "properties": _inject_intent({})}, + }, + } + + +def _master_tool_list_active_sub_agents() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "list_active_sub_agents", + "description": "列出当前多智能体会话中所有活跃/已创建的子智能体实例(agent_id/role/display_name/status)。", + "parameters": {"type": "object", "properties": _inject_intent({})}, + }, + } + + +def _master_tool_get_sub_agent_status() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "get_sub_agent_status", + "description": "查询一个或多个子智能体的详细状态。", + "parameters": { + "type": "object", + "properties": _inject_intent({ + "agent_ids": {"type": "array", "items": {"type": "integer"}, "description": "要查询的子智能体编号列表。"}, + }), + "required": ["agent_ids"], + }, + }, + } + + +MULTI_AGENT_MASTER_TOOLS: List[Dict[str, Any]] = [ + _master_tool_create_sub_agent(), + _master_tool_terminate_sub_agent(), + _master_tool_send_message_to_sub_agent(), + _master_tool_ask_sub_agent(), + _master_tool_answer_sub_agent_question(), + _master_tool_create_custom_agent(), + _master_tool_list_agents(), + _master_tool_list_active_sub_agents(), + _master_tool_get_sub_agent_status(), +] + + +# ----- 子智能体工具 ----- +def _sub_tool_ask_master() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "ask_master", + "description": ( + "向 Team Leader(主智能体)提问,工具调用会阻塞等待主智能体通过 answer_sub_agent_question 给出回答。" + "用于需要主智能体决策的场合。" + ), + "parameters": { + "type": "object", + "properties": _inject_intent({ + "question": {"type": "string", "description": "向 Team Leader 提问的内容。"}, + "question_id": {"type": "string", "description": "(可选)问题 id;不传自动生成。"}, + }), + "required": ["question"], + }, + }, + } + + +def _sub_tool_ask_other_agent() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "ask_other_agent", + "description": ( + "向另一个子智能体提问,阻塞等待对方调用 answer_other_agent 回答。" + "**注意**:调用此工具的同时,你必须在你的文本输出里向 Team Leader 输出一条汇报," + "说明你为何问、问谁、期望什么——不要偷偷沟通。" + ), + "parameters": { + "type": "object", + "properties": _inject_intent({ + "target_agent_id": {"type": "integer", "description": "目标子智能体编号。"}, + "question": {"type": "string", "description": "提问内容。"}, + "question_id": {"type": "string", "description": "(可选)问题 id。"}, + }), + "required": ["target_agent_id", "question"], + }, + }, + } + + +def _sub_tool_answer_other_agent() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "answer_other_agent", + "description": ( + "回答其他子智能体通过 ask_other_agent 提出的问题。" + "answer 内容会直接返回到对方 ask_other_agent 工具结果中。" + ), + "parameters": { + "type": "object", + "properties": _inject_intent({ + "source_agent_id": {"type": "integer", "description": "提问方 agent_id。"}, + "question_id": {"type": "string", "description": "提问消息中的 id。"}, + "answer": {"type": "string", "description": "回答内容。"}, + }), + "required": ["source_agent_id", "question_id", "answer"], + }, + }, + } + + +def _sub_tool_list_active_sub_agents() -> Dict[str, Any]: + return { + "type": "function", + "function": { + "name": "list_active_sub_agents", + "description": "查询当前多智能体会话中所有活跃/已创建的子智能体。", + "parameters": {"type": "object", "properties": _inject_intent({})}, + }, + } + + +MULTI_AGENT_SUB_AGENT_TOOLS: List[Dict[str, Any]] = [ + _sub_tool_ask_master(), + _sub_tool_ask_other_agent(), + _sub_tool_answer_other_agent(), + _sub_tool_list_active_sub_agents(), +] + + +# ----- 构造函数(供现有 tools_definition 调用) ----- +def build_master_tools_for_conversation() -> List[Dict[str, Any]]: + """返回主智能体在多智能体模式下应额外提供的工具列表。""" + return list(MULTI_AGENT_MASTER_TOOLS) + + +def build_sub_agent_tools_for_role() -> List[Dict[str, Any]]: + """返回子智能体在多智能体模式下应额外提供的工具列表。""" + return list(MULTI_AGENT_SUB_AGENT_TOOLS) \ No newline at end of file diff --git a/modules/sub_agent/manager.py b/modules/sub_agent/manager.py index 082d8ed..47c7e44 100644 --- a/modules/sub_agent/manager.py +++ b/modules/sub_agent/manager.py @@ -56,6 +56,9 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi 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) @@ -66,6 +69,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi 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: @@ -137,8 +142,16 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi 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, ) -> Dict: - """创建子智能体任务并启动协程。""" + """创建子智能体任务并启动协程。 + + 参数 multi_agent_mode: True 时启用多智能体模式。 + 参数 role_id: 多智能体模式下的角色标诶。 + 参数 display_name: 多智能体模式下的显示名(如 UI Operator_1)。 + """ validation_error = self._validate_create_params(agent_id, summary, task, deliverables_dir) if validation_error: return {"success": False, "error": validation_error} @@ -214,6 +227,25 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi 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, @@ -221,19 +253,30 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi 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, ) 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): 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) 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}") return { @@ -244,6 +287,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi "message": message, "deliverables_dir": str(deliverables_path), "run_in_background": run_in_background, + "display_name": display_name, } def wait_for_completion( @@ -482,6 +526,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi 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": @@ -497,6 +543,69 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi logger.exception(f"[SubAgent] 工具执行异常: {tool_name}") return {"success": False, "error": f"工具执行异常: {exc}"} + # ------------------------------------------------------------------ + # 多智能体模式:状态管理、外部接口、消息注入 + # ------------------------------------------------------------------ + 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: + return state + state = MultiAgentState(conversation_id=conversation_id) + self.multi_agent_states[conversation_id] = state + return state + + def get_multi_agent_state(self, conversation_id: str): + """获取该会话的多智能体状态。""" + return self.multi_agent_states.get(conversation_id) + + def drop_multi_agent_state(self, conversation_id: str) -> None: + """删除会话状态(会话结束时调用)。""" + self.multi_agent_states.pop(conversation_id, None) + + 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) + if not sub_agent: + return False + sub_agent.messages.append({"role": "user", "content": 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 实例状态。""" + # 取出当前 status(由 _finalize_task 设置) + final_task = self.tasks.get(task_id) or {} + final_status = final_task.get("status") or "terminated" + # 允许的自然退出:running -> idle(未走 _finalize) vs failed/timeout + # SubAgentTask 在多智能体模式下没有 _finalize_task,我们手动赋值 idle + # 如果状态被结 _finalize 为 failed/timeout,则保持该状态 + if final_status in TERMINAL_STATUSES: + state.mark_status(agent_id, final_status, last_output=str(final_task.get("final_result") or "")) + elif final_status == "terminated": + state.mark_status(agent_id, "terminated") + else: + state.mark_status(agent_id, "idle") + def _get_runtime_path(self, host_path: Path) -> str: """将宿主机路径映射为容器内路径(仅用于提示展示)。""" if not self.container_session or getattr(self.container_session, "mode", None) != "docker": diff --git a/modules/sub_agent/task.py b/modules/sub_agent/task.py index a5d2353..bf20b16 100644 --- a/modules/sub_agent/task.py +++ b/modules/sub_agent/task.py @@ -21,9 +21,19 @@ from utils.logger import setup_logger if TYPE_CHECKING: from modules.sub_agent.manager import SubAgentManager + from modules.multi_agent.state import MultiAgentState logger = setup_logger(__name__) +# 多智能体模式下额外加载的工具定义 +def _load_multi_agent_sub_agent_tools() -> List[Dict[str, Any]]: + try: + from modules.multi_agent.tools import build_sub_agent_tools_for_role + return build_sub_agent_tools_for_role() + except Exception as exc: + logger.warning(f"[SubAgentTask] 加载多智能体工具失败: {exc}") + return [] + class SubAgentTask: """单个后台子智能体任务。""" @@ -36,6 +46,10 @@ class SubAgentTask: system_prompt: str, model_key: Optional[str], thinking_mode: Optional[str], + *, + multi_agent_mode: bool = False, + multi_agent_state: Optional["MultiAgentState"] = None, + display_name: Optional[str] = None, ): self.manager = manager self.task_record = task_record @@ -73,6 +87,12 @@ class SubAgentTask: self._cancelled = False self._task: Optional[asyncio.Task] = None + # 多智能体模式相关字段 + self.multi_agent_mode = bool(multi_agent_mode) + self.multi_agent_state = multi_agent_state + # display_name 不传时回退为 'Agent_{agent_id}' + self.display_name = display_name or f"Agent_{self.agent_id}" + def emit(self, type_: str, data: Dict[str, Any]) -> None: """输出一行 JSONL 到 progress 文件并缓存。""" line = json.dumps({"type": type_, **data}, ensure_ascii=False) @@ -100,8 +120,13 @@ class SubAgentTask: async def _run_loop(self) -> None: client, model_key = self._build_client() - tools = list(SUB_AGENT_TOOLS) - tools.append(FINISH_TOOL) + if self.multi_agent_mode: + tools = list(SUB_AGENT_TOOLS) + tools.extend(_load_multi_agent_sub_agent_tools()) + # 多智能体模式下不要求 finish_task,自然输出结束即本轮任务结束 + else: + tools = list(SUB_AGENT_TOOLS) + tools.append(FINISH_TOOL) start_time = time.time() max_turns = 50 @@ -123,6 +148,10 @@ class SubAgentTask: if usage: self._apply_usage(usage) + # 多智能体模式:把 assistant 文本输出作为进度/完成 output 转发到主对话 + if self.multi_agent_mode and self.multi_agent_state and assistant_message.strip(): + self._forward_output_to_master(assistant_message) + final_message: Dict[str, Any] = {"role": "assistant", "content": assistant_message} if reasoning: final_message["reasoning_content"] = reasoning @@ -131,6 +160,11 @@ class SubAgentTask: self.messages.append(final_message) if not tool_calls: + # 多智能体模式:没有 tool_calls 表示本轮结束,进入 idle 状态 + if self.multi_agent_mode: + self._mark_idle() + return + # 普通模式:prompt 并要求继续 / finish_task self.messages.append({ "role": "user", "content": "如果你已经完成了任务,请调用 finish_task 工具提交完成报告。如果还没有完成,请继续执行任务。", @@ -165,6 +199,26 @@ class SubAgentTask: await self._write_failure("任务执行超过最大轮次限制", max_turns_exceeded=True) + def _forward_output_to_master(self, output_text: str) -> None: + """把子智能体的 assistant 文本输出转发成主对话的 user 消息。""" + if not self.multi_agent_state: + return + try: + from modules.multi_agent.state import build_sub_agent_output_text + msg = build_sub_agent_output_text(self.display_name, output_text.strip()) + self.multi_agent_state.push_master_message(msg) + # 同时记录到实例状态,供 list_active_sub_agents 使用 + inst = self.multi_agent_state.get_instance(self.agent_id) + if inst: + inst.last_output = output_text[:500] + except Exception as exc: + logger.warning(f"[SubAgentTask] forward output to master failed: {exc}") + + def _mark_idle(self) -> None: + """多智能体模式下,子智能体自然结束']=本轮任务 结束,进入 idle 状态。""" + if self.multi_agent_state: + self.multi_agent_state.mark_status(self.agent_id, "idle") + def _build_client(self) -> tuple: """加载模型配置并初始化 DeepSeekClient。""" config_path = self.manager.models_config_file @@ -262,9 +316,78 @@ class SubAgentTask: return {"_raw": raw} async def _execute_tool(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]: - """通过 manager 调用主进程执行工具。""" + """通过 manager 调用主进程执行工具。 + + 多智能体模式下,对于通信工具(ask_master / ask_other_agent / answer_other_agent/ + list_active_sub_agents)在主进程内直接处理,不再转发到 WebTerminal。 + """ + if self.multi_agent_mode and self.multi_agent_state: + result = await self._execute_multi_agent_tool(name, args) + if result is not None: + return result return await self.manager.execute_tool_for_sub_agent(name, args) + async def _execute_multi_agent_tool(self, name: str, args: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """处理多智能体模式专属的通信工具。返回 None 表示不 属于多智能体工具。""" + state = self.multi_agent_state + if not state: + return None + try: + if name == "ask_master": + question = str(args.get("question") or "").strip() + question_id = str(args.get("question_id") or f"ask_master_{uuid.uuid4().hex[:10]}") + if not question: + return {"success": False, "error": "question 不能为空"} + # 插入到主对话 + from modules.multi_agent.state import build_sub_agent_ask_master_text + msg = build_sub_agent_ask_master_text(self.display_name, question, question_id) + state.push_master_message(msg) + inst = state.get_instance(self.agent_id) + if inst: + inst.last_output = f"[ask_master] {question[:200]}" + # 阻塞等待回答(状态标为正在等待主智能体回答) + state.mark_status(self.agent_id, "running") + answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600)) + state.mark_status(self.agent_id, "running") + return {"success": True, "answer": answer, "question_id": question_id} + + if name == "ask_other_agent": + target_id = int(args.get("target_agent_id") or 0) + question = str(args.get("question") or "").strip() + question_id = str(args.get("question_id") or f"ask_other_{uuid.uuid4().hex[:10]}") + if not target_id or not question: + return {"success": False, "error": "参数缺失"} + # 查找目标实例 + target_inst = state.get_instance(target_id) + if not target_inst: + return {"success": False, "error": f"agent {target_id} 不存在"} + # 构造提问消息并插入到目标子对话;同时要求其在下一轮调用 answer_other_agent + from modules.multi_agent.state import build_sub_agent_ask_other_text + target_display = target_inst.display_name + msg = build_sub_agent_ask_other_text(self.display_name, target_display, question, question_id) + self.manager.inject_message_to_sub_agent(target_id, msg) + # 阻塞等待回答 + answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600)) + return {"success": True, "answer": answer, "question_id": question_id} + + if name == "answer_other_agent": + source_id = int(args.get("source_agent_id") or 0) + question_id = str(args.get("question_id") or "") + answer = str(args.get("answer") or "").strip() + if not question_id or not answer: + return {"success": False, "error": "参数缺失"} + ok = state.provide_answer(question_id, answer) + return {"success": bool(ok), "question_id": question_id} + + if name == "list_active_sub_agents": + return {"success": True, "agents": [a.to_dict() for a in state.list_all()]} + except asyncio.TimeoutError: + return {"success": False, "error": "等待回答超时", "question_id": args.get("question_id")} + except Exception as exc: + logger.exception(f"[SubAgent] 多智能体工具异常: {name}") + return {"success": False, "error": f"多智能体工具异常: {exc}"} + return None + def _update_stats(self, name: str) -> None: if name == "read_file": self.stats["files_read"] += 1 diff --git a/server/app_legacy.py b/server/app_legacy.py index a97ac4e..61af79f 100644 --- a/server/app_legacy.py +++ b/server/app_legacy.py @@ -33,6 +33,7 @@ from server.usage import usage_bp from server.status import status_bp from server.tasks import tasks_bp from server.api_v1 import api_v1_bp +from server.multi_agent import multi_agent_bp from server.socket_handlers import socketio from server.security import attach_security_hooks from werkzeug.utils import secure_filename @@ -296,6 +297,7 @@ app.register_blueprint(usage_bp) app.register_blueprint(status_bp) app.register_blueprint(tasks_bp) app.register_blueprint(api_v1_bp) +app.register_blueprint(multi_agent_bp) # 安全钩子(CSRF 校验 + 响应头) attach_security_hooks(app) diff --git a/server/multi_agent.py b/server/multi_agent.py new file mode 100644 index 0000000..5c033b8 --- /dev/null +++ b/server/multi_agent.py @@ -0,0 +1,199 @@ +"""多智能体模式 server 路由。 + +- `/multiagent/new` 返回主 SPA 入口(与 `/new` 一样返回 static/index.html) + 前端通过路径识别多智能体模式后,在创建对话时写入 metadata.multi_agent_mode=true +- `/api/multiagent/conversations` POST 创建多智能体对话(写入 metadata) +- `/api/multiagent/roles` GET 列出可用角色 +- `/api/multiagent/roles` POST 创建自定义角色 +""" +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict, List + +from flask import Blueprint, current_app, jsonify, request, session + +from server.auth_helpers import api_login_required, get_current_username +from server.context import get_user_resources + +multi_agent_bp = Blueprint("multi_agent", __name__) + + +@multi_agent_bp.route("/multiagent/new") +@api_login_required +def multi_agent_new_page(): + """多智能体模式入口,返回与 /new 相同的 SPA index.html。""" + return current_app.send_static_file("index.html") + + +@multi_agent_bp.route("/api/multiagent/roles", methods=["GET"]) +@api_login_required +def list_roles_api(): + """列出全部可用角色(预置+自定义)。""" + try: + from modules.multi_agent.role_store import list_roles + roles = list_roles() + return jsonify({ + "success": True, + "roles": [r.to_dict() for r in roles], + }) + except Exception as exc: + return jsonify({"success": False, "error": str(exc)}), 500 + + +@multi_agent_bp.route("/api/multiagent/roles", methods=["POST"]) +@api_login_required +def create_role_api(): + """创建自定义角色。body: { role_id, name, description?, body_prompt, thinking_mode? }""" + try: + from modules.multi_agent.role_store import RoleConfig, save_custom_role, list_roles + data = request.get_json() or {} + role_id = str(data.get("role_id") or "").strip() + name = str(data.get("name") or "").strip() + body_prompt = str(data.get("body_prompt") or "").strip() + description = str(data.get("description") or "").strip() + thinking_mode = str(data.get("thinking_mode") or "fast").strip() + if not role_id or not name or not body_prompt: + return jsonify({"success": False, "error": "role_id/name/body_prompt 必填"}), 400 + if thinking_mode not in {"fast", "thinking"}: + thinking_mode = "fast" + # 不允许覆盖已存在的同名角色 + existing = {r.role_id for r in list_roles()} + if role_id in existing: + return jsonify({"success": False, "error": f"角色 {role_id} 已存在"}), 409 + role = RoleConfig( + role_id=role_id, + name=name, + description=description, + body_prompt=body_prompt, + thinking_mode=thinking_mode, + ) + saved = save_custom_role(role) + return jsonify({"success": True, "role_id": role_id, "file": str(saved)}) + except Exception as exc: + return jsonify({"success": False, "error": str(exc)}), 500 + + +@multi_agent_bp.route("/api/multiagent/conversations", methods=["POST"]) +@api_login_required +def create_multi_agent_conversation(): + """创建多智能体模式对话。在 metadata 中写入 multi_agent_mode=true。 + + body: { workspace_id?, thinking_mode?, run_mode?, preserve_mode? } + """ + import time as _time + from server.conversation import _get_active_workspace_task, _resolve_target_terminal_for_workspace + from modules.personalization_manager import load_personalization_config + try: + from server.user_workspace import UserWorkspace # noqa + except Exception: + UserWorkspace = None # type: ignore + + username = get_current_username() + if not username: + return jsonify({"success": False, "error": "未登录"}), 401 + + data = request.get_json() or {} + target_workspace_id = (data.get("workspace_id") or "").strip() + + terminal, workspace = get_user_resources(username) + if not terminal or not workspace: + return jsonify({"success": False, "error": "工作区未就绪"}), 503 + + if target_workspace_id: + try: + terminal, workspace = _resolve_target_terminal_for_workspace( + username, target_workspace_id, terminal, workspace + ) + except ValueError as exc: + return jsonify({"success": False, "error": str(exc)}), 404 + except RuntimeError as exc: + return jsonify({"success": False, "error": str(exc)}), 503 + + preserve_mode = bool(data.get("preserve_mode")) + 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 + + effective_workspace_id = target_workspace_id or session.get("workspace_id") or "default" + active_task = _get_active_workspace_task(username=username, workspace_id=effective_workspace_id) + + try: + prefs = load_personalization_config(workspace.data_dir) + except Exception: + prefs = {} + + cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None) + if not cm: + return jsonify({"success": False, "error": "对话管理器未初始化"}), 500 + + safe_run_mode = run_mode + if safe_run_mode not in {"fast", "thinking", "deep"}: + candidate = (prefs or {}).get("default_run_mode") + safe_run_mode = candidate if candidate in {"fast", "thinking", "deep"} else "fast" + safe_thinking = bool(thinking_mode) if thinking_mode is not None else safe_run_mode != "fast" + default_permission_mode = (prefs or {}).get("default_permission_mode") + if default_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"): + default_permission_mode = None + + previous_cm_current = getattr(cm, "current_conversation_id", None) + + conversation_id = cm.create_conversation( + project_path=str(workspace.project_path), + thinking_mode=safe_thinking, + run_mode=safe_run_mode, + initial_messages=[], + model_key=(prefs or {}).get("default_model") or getattr(terminal, "model_key", None), + metadata_overrides={ + "permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(), + "execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(), + "multi_agent_mode": True, + }, + ) + try: + cm.current_conversation_id = previous_cm_current + except Exception: + pass + + # 触发对话列表更新事件 + try: + from server.app_legacy import socketio + socketio.emit('conversation_list_update', { + 'action': 'created', + 'conversation_id': conversation_id, + }, room=f"user_{username}") + except Exception: + pass + + return jsonify({ + "success": True, + "conversation_id": conversation_id, + "multi_agent_mode": True, + }), 201 + + +@multi_agent_bp.route("/api/multiagent/active_sub_agents", methods=["GET"]) +@api_login_required +def list_active_sub_agents_api(): + """查询当前会话所有子智能体实例(多智能体模式专用)。""" + username = get_current_username() + if not username: + return jsonify({"success": False, "error": "未登录"}), 401 + conversation_id = (request.args.get("conversation_id") or "").strip() + if not conversation_id: + return jsonify({"success": False, "error": "缺少 conversation_id 参数"}), 400 + terminal, _ = get_user_resources(username) + if not terminal: + return jsonify({"success": False, "error": "工作区未就绪"}), 503 + sub_agent_manager = getattr(terminal, "sub_agent_manager", None) + if not sub_agent_manager: + return jsonify({"success": False, "error": "子智能体管理器未就绪"}), 503 + state = sub_agent_manager.get_multi_agent_state(conversation_id) + if not state: + return jsonify({"success": True, "agents": []}) + return jsonify({ + "success": True, + "agents": [a.to_dict() for a in state.list_all()], + }) + + +__all__ = ["multi_agent_bp"] \ No newline at end of file diff --git a/static/src/app/methods/ui/route.ts b/static/src/app/methods/ui/route.ts index 3dd72bc..e221e50 100644 --- a/static/src/app/methods/ui/route.ts +++ b/static/src/app/methods/ui/route.ts @@ -31,6 +31,88 @@ export const routeMethods = { this.currentConversationTitle = ''; this.titleTypingText = ''; const path = window.location.pathname.replace(/^\/+/, ''); + // 检查多智能体模式入口 + if (path === 'multiagent/new' || path === 'multiagent') { + this.multiAgentMode = true; + this.currentConversationId = null; + this.currentConversationTitle = '多智能体模式'; + this.titleReady = true; + this.suppressTitleTyping = false; + this.startTitleTyping('多智能体模式', { animate: false }); + this.initialRouteResolved = true; + this.refreshBlankHeroState(); + // 多智能体模式下自动创建一个带 metadata.multi_agent_mode=true 的新对话 + try { + const resp = await fetch('/api/multiagent/conversations', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}) + }); + const result = await resp.json(); + if (result && result.success && result.conversation_id) { + this.currentConversationId = result.conversation_id; + // 拉取完整会话信息 + try { + const loadResp = await fetch(`/api/conversations/${result.conversation_id}/load`, { method: 'PUT' }); + const loadResult = await loadResp.json(); + if (loadResult.success) { + if (typeof loadResult.run_mode === 'string') { + this.runMode = loadResult.run_mode; + this.thinkingMode = typeof loadResult.thinking_mode === 'boolean' ? loadResult.thinking_mode : loadResult.run_mode !== 'fast'; + } + if (typeof loadResult.model_key === 'string' && loadResult.model_key) this.modelSet(loadResult.model_key); + this.currentConversationTitle = loadResult.title || '多智能体模式'; + this.startTitleTyping(this.currentConversationTitle, { animate: false }); + history.replaceState({ conversationId: result.conversation_id }, '', `/multiagent/${result.conversation_id.replace(/^conv_/, '')}`); + this.logMessageState('bootstrapRoute:multi-agent-loaded'); + } + } catch (_e) { + // 加载失败也继续,主对话 已创建 + } + } + } catch (error) { + console.warn('[multiagent] 初始化多智能体对话失败:', error); + } + await this.restoreComposerDraftState('bootstrap-route:multiagent'); + return; + } + // 当 URL 是 /multiagent/conv_xxx 时也走多智能体模式 + if (path.startsWith('multiagent/')) { + this.multiAgentMode = true; + const convPart = path.slice('multiagent/'.length); + const convId = convPart.startsWith('conv_') ? convPart : `conv_${convPart}`; + try { + const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' }); + const result = await resp.json(); + if (result.success) { + if (typeof result.run_mode === 'string') { + this.runMode = result.run_mode; + this.thinkingMode = typeof result.thinking_mode === 'boolean' ? result.thinking_mode : result.run_mode !== 'fast'; + } else if (typeof result.thinking_mode === 'boolean') { + this.thinkingMode = result.thinking_mode; + this.runMode = result.thinking_mode ? 'thinking' : 'fast'; + } + if (typeof result.model_key === 'string' && result.model_key) this.modelSet(result.model_key); + this.currentConversationId = convId; + this.currentConversationTitle = result.title || '多智能体模式'; + this.titleReady = true; + this.suppressTitleTyping = false; + this.startTitleTyping(this.currentConversationTitle, { animate: false }); + history.replaceState({ conversationId: convId }, '', `/multiagent/${this.stripConversationPrefix(convId)}`); + this.initialRouteResolved = true; + await this.restoreComposerDraftState('bootstrap-route:multiagent-existing'); + return; + } + } catch (error) { + console.warn('[multiagent] 加载多智能体对话失败:', error); + } + // 加载失败回退到 multiagent/new 路径 + history.replaceState({}, '', '/multiagent/new'); + window.location.reload(); + return; + } + // 非多智能体模式:清除标志 + this.multiAgentMode = false; if (!path || this.isExplicitNewConversationRoute()) { this.currentConversationId = null; this.currentConversationTitle = '新对话'; diff --git a/static/src/app/state.ts b/static/src/app/state.ts index 1f93721..0fbdb12 100644 --- a/static/src/app/state.ts +++ b/static/src/app/state.ts @@ -6,6 +6,8 @@ export function dataState() { // 路由相关 initialRouteResolved: false, dropToolEvents: false, + // 多智能体模式开关 + multiAgentMode: false, // 轮询模式标志(禁用 WebSocket 事件处理) usePollingMode: true, diff --git a/static/src/auth/LoginApp.vue b/static/src/auth/LoginApp.vue index 4ffbee8..c9c7633 100644 --- a/static/src/auth/LoginApp.vue +++ b/static/src/auth/LoginApp.vue @@ -38,6 +38,14 @@ 宿主机模式(免登录) + +
{{ error }}
@@ -96,6 +104,11 @@ const login = async () => { } }; +const enterMultiAgent = () => { + // 直接跳转,后端 @login_required 会确保已登录后重定向回来 + window.location.href = '/multiagent/new'; +}; + const hostLogin = async () => { hostSubmitting.value = true; error.value = '';