From fe6fba3958065fdfc13fd80bfb7d5722bf12ea38 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 13 Jul 2026 14:01:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(multi=5Fagent):=20=E6=8F=90=E5=8F=96prompt?= =?UTF-8?q?=E3=80=81=E6=81=A2=E5=A4=8D=E4=BB=BB=E5=8A=A1=E3=80=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BE=A7=E8=BE=B9=E6=A0=8F=E4=B8=8Esleep=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将多智能体/子智能体prompt从代码提取到prompts/multi_agent/和prompts/sub_agent/ - 多智能体模式添加可用的子智能体动态prompt并冻结 - 重启后自动从conversation.json恢复多智能体idle任务 - 修复新对话侧边栏显示其他对话子智能体的问题 - 简化子智能体弹窗输出样式 - sleep工具在多智能体模式下移除wait_sub_agent_ids参数 --- core/main_terminal_parts/context/messages.py | 12 ++ .../tools_definition/core_tools.py | 96 ++++++--- core/main_terminal_parts/tools_execution.py | 28 +-- modules/multi_agent/prompts.py | 203 +++++++----------- modules/sub_agent/manager.py | 159 ++++++++++++++ modules/sub_agent/prompts.py | 145 +++++-------- modules/sub_agent/state.py | 29 +++ prompts/multi_agent/available_agents.txt | 7 + prompts/multi_agent/master.txt | 74 +++++++ prompts/multi_agent/sub_agent.txt | 55 +++++ prompts/sub_agent/system.txt | 77 +++++++ prompts/sub_agent/user_message.txt | 13 ++ server/conversation.py | 5 +- .../overlay/SubAgentActivityDialog.vue | 30 +-- 14 files changed, 632 insertions(+), 301 deletions(-) create mode 100644 prompts/multi_agent/available_agents.txt create mode 100644 prompts/multi_agent/master.txt create mode 100644 prompts/multi_agent/sub_agent.txt create mode 100644 prompts/sub_agent/system.txt create mode 100644 prompts/sub_agent/user_message.txt diff --git a/core/main_terminal_parts/context/messages.py b/core/main_terminal_parts/context/messages.py index dfc9ea5..8605c46 100644 --- a/core/main_terminal_parts/context/messages.py +++ b/core/main_terminal_parts/context/messages.py @@ -48,6 +48,7 @@ from modules.terminal_manager import TerminalManager from modules.todo_manager import TodoManager from modules.sub_agent import SubAgentManager from modules.webpage_extractor import extract_webpage_content, tavily_extract +from modules.multi_agent.prompts import build_available_agents_prompt from modules.ocr_client import OCRClient from modules.easter_egg_manager import EasterEggManager from modules.personalization_manager import ( @@ -288,6 +289,17 @@ class MessagesMixin: messages.append({"role": "system", "content": multi_agent_prompt}) except Exception as exc: logger.warning(f"[messages] 注入多智能体 prompt 失败: {exc}") + + # 可用的子智能体角色(动态 prompt,第一个用户消息后冻结) + try: + available_agents_prompt = self._get_or_init_frozen_prompt( + "frozen_available_agents_prompt", + lambda: build_available_agents_prompt() or "", + ) + if available_agents_prompt: + messages.append({"role": "system", "content": available_agents_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/core_tools.py b/core/main_terminal_parts/tools_definition/core_tools.py index 7eb6dc8..c0cbc4f 100644 --- a/core/main_terminal_parts/tools_definition/core_tools.py +++ b/core/main_terminal_parts/tools_definition/core_tools.py @@ -90,38 +90,74 @@ DISABLE_LENGTH_CHECK = True class ToolsDefinitionCoreToolsMixin: + + def _build_sleep_tool_definition(self) -> Dict: + """根据运行模式构建 sleep 工具定义。 + + 多智能体模式下子智能体不会调用 finish_task 结束,因此不提供 + wait_sub_agent_ids 参数;常规模式保留完整功能。 + """ + is_multi_agent = getattr(self, "multi_agent_mode", False) + if is_multi_agent: + return { + "type": "function", + "function": { + "name": "sleep", + "description": "等待工具。两种模式二选一:1) seconds:短暂延迟;2) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果。若同时提供多个等待参数会报错。", + "parameters": { + "type": "object", + "properties": self._inject_intent({ + "seconds": { + "type": "number", + "description": "等待的秒数,可以是小数(如0.2秒)。建议范围:0.1-10秒" + }, + "wait_runcommand_id": { + "type": "string", + "description": "等待指定后台 run_command 的 command_id 结束后返回。" + }, + "reason": { + "type": "string", + "description": "等待的原因说明(可选)" + } + }), + "required": [] + } + } + } + return { + "type": "function", + "function": { + "name": "sleep", + "description": "等待工具。三种模式三选一:1) seconds:短暂延迟;2) wait_sub_agent_ids:等待指定子智能体全部结束并直接返回结果;3) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果。若同时提供多个等待参数会报错。", + "parameters": { + "type": "object", + "properties": self._inject_intent({ + "seconds": { + "type": "number", + "description": "等待的秒数,可以是小数(如0.2秒)。建议范围:0.1-10秒" + }, + "wait_sub_agent_ids": { + "type": "array", + "items": {"type": "integer"}, + "description": "等待这些子智能体全部结束后返回(可提供一个或多个编号)。" + }, + "wait_runcommand_id": { + "type": "string", + "description": "等待指定后台 run_command 的 command_id 结束后返回。" + }, + "reason": { + "type": "string", + "description": "等待的原因说明(可选)" + } + }), + "required": [] + } + } + } + def _build_core_tools(self) -> List[Dict]: return [ - { - "type": "function", - "function": { - "name": "sleep", - "description": "等待工具。三种模式三选一:1) seconds:短暂延迟;2) wait_sub_agent_ids:等待指定子智能体全部结束并直接返回结果;3) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果。若同时提供多个等待参数会报错。", - "parameters": { - "type": "object", - "properties": self._inject_intent({ - "seconds": { - "type": "number", - "description": "等待的秒数,可以是小数(如0.2秒)。建议范围:0.1-10秒" - }, - "wait_sub_agent_ids": { - "type": "array", - "items": {"type": "integer"}, - "description": "等待这些子智能体全部结束后返回(可提供一个或多个编号)。" - }, - "wait_runcommand_id": { - "type": "string", - "description": "等待指定后台 run_command 的 command_id 结束后返回。" - }, - "reason": { - "type": "string", - "description": "等待的原因说明(可选)" - } - }), - "required": [] - } - } - }, + self._build_sleep_tool_definition(), { "type": "function", diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index d9d70aa..70d841f 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -1099,7 +1099,12 @@ class MainTerminalToolsExecutionMixin: "error": "sleep 的等待参数互斥:seconds / wait_sub_agent_ids / wait_runcommand_id 只能提供一个" } elif wait_sub_agent_ids: - if not isinstance(wait_sub_agent_ids, list) or not wait_sub_agent_ids: + if getattr(self, "multi_agent_mode", False): + result = { + "success": False, + "error": "多智能体模式下 sleep 工具不支持 wait_sub_agent_ids。请通过 list_active_sub_agents / get_sub_agent_status 查看状态,或直接向子智能体发送消息。" + } + elif not isinstance(wait_sub_agent_ids, list) or not wait_sub_agent_ids: result = {"success": False, "error": "wait_sub_agent_ids 必须是非空数组"} else: normalized_ids = [] @@ -1129,27 +1134,6 @@ class MainTerminalToolsExecutionMixin: task_ids.append(task.get("task_id")) if missing: result = {"success": False, "error": f"未找到对应子智能体: {missing}"} - elif getattr(self, "multi_agent_mode", False): - # 多智能体模式:子智能体通过自然输出结束,不调用 finish_task。 - # sleep 不再阻塞等待,而是直接返回各实例最近一次输出内容。 - conv_id = self.context_manager.current_conversation_id - ma_state = manager.get_multi_agent_state(conv_id) - outputs = [] - for aid in normalized_ids: - inst = ma_state.get_instance(aid) if ma_state else None - outputs.append({ - "agent_id": aid, - "display_name": inst.display_name if inst else f"Agent_{aid}", - "status": inst.status if inst else "unknown", - "last_output": inst.last_output if inst else "", - }) - result = { - "success": True, - "mode": "wait_sub_agent_ids", - "agent_ids": normalized_ids, - "outputs": outputs, - "message": f"已获取 {len(normalized_ids)} 个子智能体最近一次输出" - } else: wait_results = [] waited_task_ids = [] diff --git a/modules/multi_agent/prompts.py b/modules/multi_agent/prompts.py index 44256a8..7c99990 100644 --- a/modules/multi_agent/prompts.py +++ b/modules/multi_agent/prompts.py @@ -1,134 +1,55 @@ -"""多智能体模式的系统提示词。""" +"""多智能体模式的系统提示词。 + +所有 prompt 正文均从 prompts/multi_agent/ 下的文本文件加载,避免在代码中硬编码。 +""" from __future__ import annotations -from typing import Optional +from pathlib import Path +from typing import List, Optional + +try: + from config import PROMPTS_DIR +except ImportError: + import sys + + project_root = Path(__file__).resolve().parents[2] + if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + from config import PROMPTS_DIR -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_PROMPTS_DIR = Path(PROMPTS_DIR) / "multi_agent" +_TEMPLATE_CACHE: dict[str, str] = {} -MULTI_AGENT_SUB_AGENT_PROMPT_BODY = """# 多智能体身份 +def _load_template(name: str) -> str: + """从 prompts/multi_agent/.txt 加载模板,带缓存。""" + cached = _TEMPLATE_CACHE.get(name) + if cached is not None: + return cached -你是智能体集群团队的一员。你的团队通过分工协作完成复杂任务,主智能体 **Team Leader** 负责督导全局。 + template_path = _MULTI_AGENT_PROMPTS_DIR / f"{name}.txt" + if not template_path.exists(): + raise FileNotFoundError(f"多智能体 prompt 模板缺失: {template_path}") -# 在任务中 + content = template_path.read_text(encoding="utf-8") + _TEMPLATE_CACHE[name] = content + return content -- 不要频繁输出内容,不重要的内容会污染主智能体上下文 -- 只汇报关键步骤 -- 任务完成后给出详细结论 -- 自然结束输出即本轮任务结束;上下文会被保留,Team Leader 可能会再次发消息让你继续 -# 沟通工具 +def _format_template(name: str, **kwargs) -> str: + """加载模板并用 str.format 填充占位符。""" + template = _load_template(name) + return template.format(**kwargs) -- **需要 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 __getattr__(name: str) -> str: + if name == "MULTI_AGENT_MASTER_PROMPT_BODY": + return _load_template("master") + if name == "MULTI_AGENT_SUB_AGENT_PROMPT_BODY": + return _load_template("sub_agent") + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") def build_multi_agent_master_prompt(workspace_path: str, base: str = "") -> str: @@ -137,9 +58,10 @@ def build_multi_agent_master_prompt(workspace_path: str, base: str = "") -> str: `base` 一般为现有 MainTerminal 的 base 提示词(环境/工具概览等), 我们在末尾追加多智能体模式专属正文。 """ + body = _load_template("master") 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" + return f"{base.rstrip()}\n\n{body}\n" + return f"{body}\n" def build_multi_agent_sub_agent_prompt(role_body: str, display_name: str, workspace_path: str) -> str: @@ -148,9 +70,36 @@ def build_multi_agent_sub_agent_prompt(role_body: str, display_name: str, worksp `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 + return _format_template("sub_agent", display_name=display_name, role_body=role_body.strip()) + + +def build_available_agents_prompt() -> str: + """构造「可用的子智能体角色」动态 prompt。 + + 列出当前可创建的所有角色名称与说明,供 Team Leader 参考。 + 若没有任何可用角色,返回空字符串。 + """ + try: + from modules.multi_agent.role_store import list_roles + + roles = list_roles() + except Exception: + return "" + + if not roles: + return "" + + lines: List[str] = [] + for role in roles: + name = getattr(role, "name", "") or "" + description = getattr(role, "description", "") or "" + role_id = getattr(role, "role_id", "") or "" + if description: + lines.append(f"- {name}(role_id: {role_id}):{description}") + else: + lines.append(f"- {name}(role_id: {role_id})") + + if not lines: + return "" + + return _format_template("available_agents", agents_list="\n".join(lines)) diff --git a/modules/sub_agent/manager.py b/modules/sub_agent/manager.py index 1bcaa3f..7a53697 100644 --- a/modules/sub_agent/manager.py +++ b/modules/sub_agent/manager.py @@ -78,6 +78,11 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi self.reconcile_task_states() except Exception: pass + try: + self.restore_running_tasks() + except Exception: + logger.exception("[SubAgentManager] 恢复运行中子智能体任务失败") + pass # ------------------------------------------------------------------ # 生命周期与事件循环 @@ -230,6 +235,9 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi "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", "container_name": None, } @@ -567,6 +575,157 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi 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): + 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) + + 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 + # ------------------------------------------------------------------ # 多智能体模式:状态管理、外部接口、消息注入 # ------------------------------------------------------------------ diff --git a/modules/sub_agent/prompts.py b/modules/sub_agent/prompts.py index ea089aa..2057466 100644 --- a/modules/sub_agent/prompts.py +++ b/modules/sub_agent/prompts.py @@ -1,11 +1,49 @@ -"""子智能体提示词构建。""" +"""子智能体提示词构建。 +所有 prompt 正文均从 prompts/sub_agent/ 下的文本文件加载,避免在代码中硬编码。 +""" from __future__ import annotations import platform from datetime import datetime +from pathlib import Path from typing import Optional +try: + from config import PROMPTS_DIR +except ImportError: + import sys + + project_root = Path(__file__).resolve().parents[2] + if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + from config import PROMPTS_DIR + + +_SUB_AGENT_PROMPTS_DIR = Path(PROMPTS_DIR) / "sub_agent" +_TEMPLATE_CACHE: dict[str, str] = {} + + +def _load_template(name: str) -> str: + """从 prompts/sub_agent/.txt 加载模板,带缓存。""" + cached = _TEMPLATE_CACHE.get(name) + if cached is not None: + return cached + + template_path = _SUB_AGENT_PROMPTS_DIR / f"{name}.txt" + if not template_path.exists(): + raise FileNotFoundError(f"子智能体 prompt 模板缺失: {template_path}") + + content = template_path.read_text(encoding="utf-8") + _TEMPLATE_CACHE[name] = content + return content + + +def _format_template(name: str, **kwargs) -> str: + """加载模板并用 str.format 填充占位符。""" + template = _load_template(name) + return template.format(**kwargs) + def build_user_message( agent_id: int, @@ -15,100 +53,23 @@ def build_user_message( timeout_seconds: int, ) -> str: """构建发送给子智能体的用户消息。""" - return f"""你是子智能体 #{agent_id},负责完成以下任务: - -**任务摘要**:{summary} - -**任务详情**: -{task} - -**交付目录**:{deliverables_path} -请将所有生成的文件保存到此目录。 - -**超时时间**:{timeout_seconds} 秒 - -完成任务后,请调用 finish_task 工具提交完成报告。""" + return _format_template( + "user_message", + agent_id=agent_id, + summary=summary, + task=task, + deliverables_path=deliverables_path, + timeout_seconds=timeout_seconds, + ) def build_system_prompt(workspace_path: str) -> str: """构建子智能体的系统提示。""" system_info = f"{platform.system()} {platform.release()}" current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - return f"""你是一个专注的子智能体,负责独立完成分配的任务。 - -# 身份定位 - -你是主智能体创建的子智能体,拥有完整的工具能力(读写文件、执行命令、搜索网页等)。你的职责是专注完成分配的单一任务,不要偏离任务目标。 - -# 工作流程 - -1. **理解任务**:仔细阅读任务描述,明确目标和要求 -2. **制定计划**:规划完成任务的步骤 -3. **执行任务**:使用工具完成各个步骤 -4. **生成交付**:将所有结果文件放到指定的交付目录 -5. **提交报告**:使用 finish_task 工具提交完成报告并退出 - -# 工作原则 - -## 专注性 -- 只完成分配的任务,不要做额外的工作 -- 不要尝试与用户对话或询问问题 -- 遇到问题时,在能力范围内解决或在报告中说明 - -## 独立性 -- 你与主智能体共享工作区,可以访问所有文件 -- 你的工作范围应该与其他子智能体不重叠 -- 不要修改任务描述之外的文件 - -## 效率性 -- 直接开始工作,不要过度解释 -- 合理使用工具,避免重复操作 -- 注意超时限制,在时间内完成核心工作 - -## 完整性 -- 确保交付目录中的文件完整可用 -- 生成的文档要清晰、格式正确 -- 代码要包含必要的注释和说明 - -# 交付要求 - -所有结果文件必须放在指定的交付目录中,包括: -- 主要成果文件(文档、代码、报告等) -- 支持文件(数据、配置、示例等) -- 不要在交付目录外创建文件 - -# 完成任务 - -任务完成后,必须调用 finish_task 工具: -- success: 是否成功完成 -- summary: 完成摘要(说明做了什么、生成了什么) - -调用 finish_task 后,你会立即退出,无法继续工作。 - -# 工具使用 - -你拥有以下工具能力: -- read_file: 读取文件内容 -- write_file / edit_file: 创建或修改文件 -- search_workspace: 搜索文件和代码 -- run_command: 执行终端命令 -- web_search / extract_webpage: 搜索和提取网页内容 -- read_mediafile: 读取图片/视频文件 -- finish_task: 完成任务并退出(必须调用) - -# 注意事项 - -1. **结果传达**:你在运行期间产生的记录与输出不会被直接传递给主智能体。务必把所有需要传达的信息写进 `finish_task` 工具的 `summary` 字段,以及交付目录中的落盘文件里。 -2. **不要无限循环**:如果任务无法完成,说明原因并提交报告 -3. **不要超出范围**:只操作任务描述中指定的文件/目录 -4. **不要等待输入**:你是自主运行的,不会收到用户的进一步指令 -5. **注意时间限制**:超时会被强制终止,优先完成核心工作 - -# 当前环境 - -- 工作区路径: {workspace_path} -- 系统: {system_info} -- 当前时间: {current_time} - -现在开始执行任务。""" + return _format_template( + "system", + workspace_path=workspace_path, + system_info=system_info, + current_time=current_time, + ) diff --git a/modules/sub_agent/state.py b/modules/sub_agent/state.py index 0b4467a..1cb351c 100644 --- a/modules/sub_agent/state.py +++ b/modules/sub_agent/state.py @@ -51,6 +51,23 @@ class SubAgentStateMixin: self.tasks = merged_tasks self.conversation_agents = loaded_agents + # 恢复多智能体运行态(如果状态文件包含) + try: + from modules.multi_agent.state import MultiAgentState + + manager = self + multi_agent_states = getattr(manager, "multi_agent_states", None) + if multi_agent_states is not None and isinstance(multi_agent_states, dict): + loaded_ma_states = data.get("multi_agent_states", {}) + for conv_id, snapshot in loaded_ma_states.items(): + try: + if isinstance(snapshot, dict): + multi_agent_states[conv_id] = MultiAgentState.from_snapshot(snapshot) + except Exception as exc: + logger.warning(f"恢复多智能体状态失败 {conv_id}: {exc}") + except Exception as exc: + logger.warning(f"加载多智能体状态失败: {exc}") + if self.tasks: migrated = False for task in self.tasks.values(): @@ -72,6 +89,18 @@ class SubAgentStateMixin: "tasks": self.tasks, "conversation_agents": self.conversation_agents, } + # 多智能体运行态持久化 + try: + manager = self + multi_agent_states = getattr(manager, "multi_agent_states", None) + if multi_agent_states: + payload["multi_agent_states"] = { + conv_id: state.to_snapshot() + for conv_id, state in multi_agent_states.items() + if isinstance(state, object) and hasattr(state, "to_snapshot") + } + except Exception as exc: + logger.warning(f"保存多智能体状态失败: {exc}") try: self.state_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") except Exception as exc: diff --git a/prompts/multi_agent/available_agents.txt b/prompts/multi_agent/available_agents.txt new file mode 100644 index 0000000..fed0804 --- /dev/null +++ b/prompts/multi_agent/available_agents.txt @@ -0,0 +1,7 @@ +# 可用的子智能体角色 + +当前你可以创建以下子智能体角色来协作完成任务: + +{agents_list} + +创建子智能体时请使用 `create_sub_agent` 工具,并传入合适的 `role_id`。 diff --git a/prompts/multi_agent/master.txt b/prompts/multi_agent/master.txt new file mode 100644 index 0000000..b84c28d --- /dev/null +++ b/prompts/multi_agent/master.txt @@ -0,0 +1,74 @@ +# 多智能体模式 + +你是 **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 里要求子智能体「如要向其他子智能体提问,必须同时直接给你输出一条汇报」,这样你能掌握全局。但你**不需要**手动转发它们之间的问答。 diff --git a/prompts/multi_agent/sub_agent.txt b/prompts/multi_agent/sub_agent.txt new file mode 100644 index 0000000..dedc7c0 --- /dev/null +++ b/prompts/multi_agent/sub_agent.txt @@ -0,0 +1,55 @@ +# 多智能体身份 + +你是智能体集群团队的一员。你的团队通过分工协作完成复杂任务,主智能体 **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 文字输出都会被自动捕获并以如下格式插入到主对话: + +``` +来自 {display_name} 的任务进度输出 +id: out_xxxxxxxx + +<{display_name}> + +{{你的输出}} + +{display_name}> +``` + +你不需要自己包裹 XML,直接输出正文即可。 + +# 你的显示名 + +你的显示名是 `{display_name}`。 + +# 你的专属设定 + +{role_body} diff --git a/prompts/sub_agent/system.txt b/prompts/sub_agent/system.txt new file mode 100644 index 0000000..33714d8 --- /dev/null +++ b/prompts/sub_agent/system.txt @@ -0,0 +1,77 @@ +你是一个专注的子智能体,负责独立完成分配的任务。 + +# 身份定位 + +你是主智能体创建的子智能体,拥有完整的工具能力(读写文件、执行命令、搜索网页等)。你的职责是专注完成分配的单一任务,不要偏离任务目标。 + +# 工作流程 + +1. **理解任务**:仔细阅读任务描述,明确目标和要求 +2. **制定计划**:规划完成任务的步骤 +3. **执行任务**:使用工具完成各个步骤 +4. **生成交付**:将所有结果文件放到指定的交付目录 +5. **提交报告**:使用 finish_task 工具提交完成报告并退出 + +# 工作原则 + +## 专注性 +- 只完成分配的任务,不要做额外的工作 +- 不要尝试与用户对话或询问问题 +- 遇到问题时,在能力范围内解决或在报告中说明 + +## 独立性 +- 你与主智能体共享工作区,可以访问所有文件 +- 你的工作范围应该与其他子智能体不重叠 +- 不要修改任务描述之外的文件 + +## 效率性 +- 直接开始工作,不要过度解释 +- 合理使用工具,避免重复操作 +- 注意超时限制,在时间内完成核心工作 + +## 完整性 +- 确保交付目录中的文件完整可用 +- 生成的文档要清晰、格式正确 +- 代码要包含必要的注释和说明 + +# 交付要求 + +所有结果文件必须放在指定的交付目录中,包括: +- 主要成果文件(文档、代码、报告等) +- 支持文件(数据、配置、示例等) +- 不要在交付目录外创建文件 + +# 完成任务 + +任务完成后,必须调用 finish_task 工具: +- success: 是否成功完成 +- summary: 完成摘要(说明做了什么、生成了什么) + +调用 finish_task 后,你会立即退出,无法继续工作。 + +# 工具使用 + +你拥有以下工具能力: +- read_file: 读取文件内容 +- write_file / edit_file: 创建或修改文件 +- search_workspace: 搜索文件和代码 +- run_command: 执行终端命令 +- web_search / extract_webpage: 搜索和提取网页内容 +- read_mediafile: 读取图片/视频文件 +- finish_task: 完成任务并退出(必须调用) + +# 注意事项 + +1. **结果传达**:你在运行期间产生的记录与输出不会被直接传递给主智能体。务必把所有需要传达的信息写进 `finish_task` 工具的 `summary` 字段,以及交付目录中的落盘文件里。 +2. **不要无限循环**:如果任务无法完成,说明原因并提交报告 +3. **不要超出范围**:只操作任务描述中指定的文件/目录 +4. **不要等待输入**:你是自主运行的,不会收到用户的进一步指令 +5. **注意时间限制**:超时会被强制终止,优先完成核心工作 + +# 当前环境 + +- 工作区路径: {workspace_path} +- 系统: {system_info} +- 当前时间: {current_time} + +现在开始执行任务。 diff --git a/prompts/sub_agent/user_message.txt b/prompts/sub_agent/user_message.txt new file mode 100644 index 0000000..dc3bbcd --- /dev/null +++ b/prompts/sub_agent/user_message.txt @@ -0,0 +1,13 @@ +你是子智能体 #{agent_id},负责完成以下任务: + +**任务摘要**:{summary} + +**任务详情**: +{task} + +**交付目录**:{deliverables_path} +请将所有生成的文件保存到此目录。 + +**超时时间**:{timeout_seconds} 秒 + +完成任务后,请调用 finish_task 工具提交完成报告。 diff --git a/server/conversation.py b/server/conversation.py index 859e4f1..7ec1236 100644 --- a/server/conversation.py +++ b/server/conversation.py @@ -1672,8 +1672,9 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s pass conversation_id = terminal.context_manager.current_conversation_id data = manager.get_overview(conversation_id=conversation_id) - if not data: - # 若当前对话暂无数据但存在运行中子智能体,回退为全局运行态,避免面板空白 + # 仅在未绑定具体对话时(如全新会话尚未创建 conversation_id),才回退为全局运行态; + # 否则只显示当前对话关联的子智能体,避免新对话看到其他对话的任务。 + if not data and not conversation_id: all_overview = manager.get_overview(conversation_id=None) if all_overview: terminal_statuses = TERMINAL_STATUSES.union({"terminated"}) diff --git a/static/src/components/overlay/SubAgentActivityDialog.vue b/static/src/components/overlay/SubAgentActivityDialog.vue index dc36665..41df30e 100644 --- a/static/src/components/overlay/SubAgentActivityDialog.vue +++ b/static/src/components/overlay/SubAgentActivityDialog.vue @@ -41,11 +41,6 @@ @click="handleItemClick(item)" > - - 结束汇报 - 进度输出 - {{ expandedOutputs.has(item.key) ? '点击收起' : '点击展开' }} - {{ item.content }} @@ -257,36 +252,15 @@ const timelineItems = computed(() => { gap: 8px; } .subagent-output-item { - background: var(--surface-soft); - border: 1px solid var(--border-default); - border-radius: 8px; - padding: 10px 12px; + display: block; cursor: pointer; } -.subagent-output-meta { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - margin-bottom: 6px; - color: var(--text-muted); -} -.subagent-output-final { - color: var(--accent); - font-weight: 600; -} -.subagent-output-progress { - color: var(--text-secondary); -} -.subagent-output-hint { - margin-left: auto; - opacity: 0.7; -} .subagent-output-content { color: var(--text-primary); white-space: pre-wrap; word-break: break-word; line-height: 1.5; + text-align: left; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3;