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"{tag}>\n"
+ f"{display_name}>"
+ )
+
+ 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 = '';