feat(multi-agent): 在现有架构上直接实现多智能体模式实验功能

放弃完全隔离策略,改为在现有 MainTerminal/SubAgentManager/SubAgentTask 主链路
按对话级开关 metadata.multi_agent_mode=true 增加多智能体分支。

新增模块:
- modules/multi_agent/__init__.py: 模块入口
- modules/multi_agent/role_store.py: 角色 Markdown Frontmatter 解析与归档
- modules/multi_agent/state.py: 多智能体会话状态机与消息格式化
- modules/multi_agent/prompts.py: 主智能体(Team Leader) + 子智能体提示词
- modules/multi_agent/tools.py: 9 个主智能体工具 + 4 个子智能体工具定义
- server/multi_agent.py: /multiagent/new 页面 + /api/multiagent/* 蓝图

现有代码改动:
- modules/sub_agent/task.py: 扩展 multi_agent_mode/multi_agent_state/display_name 字段,
  增加 ask_master/ask_other_agent/answer_other_agent/list_active_sub_agents 工具处理逻辑,
  子智能体自然结束 assistant 输出即本轮结束(不调用 finish_task),上下文保留。
- modules/sub_agent/manager.py: create_sub_agent 增加 multi_agent_mode/role_id/display_name 参数,
  增加 get_or_create_multi_agent_state/get_multi_agent_state/inject_message_to_sub_agent/_on_multi_agent_task_done 方法。
- core/main_terminal_parts/tools_definition/agent_tools.py: 多智能体模式下用 modules.multi_agent.tools 替换旧版工具集。
- core/main_terminal_parts/context/messages.py: 多智能体模式下追加 Team Leader 系统提示词。
- core/main_terminal_parts/tools_execution.py: create_sub_agent handler 增加多智能体分支,新增 send_message_to_sub_agent/ask_sub_agent/answer_sub_agent_question/create_custom_agent/list_agents/list_active_sub_agents handler。
- core/web_terminal.py: load_conversation 时检测 metadata.multi_agent_mode 设置 self.multi_agent_mode。
- server/app_legacy.py: 注册 multi_agent_bp 蓝图。

前端改动:
- static/src/auth/LoginApp.vue: 登录页增加'多智能体模式(beta)'按钮
- static/src/app/methods/ui/route.ts: 识别 /multiagent/new 和 /multiagent/conv_xxx 路径,进入多智能体模式并创建带 metadata.multi_agent_mode=true 的对话
- static/src/app/state.ts: 增加 multiAgentMode 状态字段

数据:
- ~/.astrion/astrion/host/mutiagents/agents/: 4 个预置角色 ui-operator / full-stack-engineer / code-reviewer / researcher
- ~/.astrion/astrion/host/mutiagents/conversations/: 会话数据

验证:所有 Python 文件语法检查通过;冒烟测试 test.test_server_refactor_smoke 6 项全通过;前端构建通过(6.04s);模块导入与功能断言测试全部通过。
This commit is contained in:
JOJO 2026-07-12 03:26:02 +08:00
parent e8b857f3ae
commit 811974d6e7
16 changed files with 1866 additions and 52 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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"))

View File

@ -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
# 重置相关状态

View File

@ -0,0 +1,53 @@
"""多智能体模式核心实现。
conversations metadata.multi_agent_mode = true 触发启用
不另起隔离链路直接复用现有 MainTerminalSubAgentManagerSubAgentTask
但通过对话级开关注入多智能体版工具集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",
]

View File

@ -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` | 查询指定子智能体的详细状态 |
**注意**你现在仍拥有原本的全部工具文件读写终端搜索MCPskillmemory 以上只列出多智能体模式新增的工具它们**替换**了原有的 `create_sub_agent` / `close_sub_agent` / `get_sub_agent_status`使用语义有变化
## 你会收到的消息格式
子智能体输出每轮 assistant 文字输出都会通过 user 消息插到你的对话里
```
来自 UI Operator_1 的任务进度输出
id: out_xxxxxxxx
<UI Operator_1>
<Output>
我现在开始分析现有设计风格...
</Output>
</UI Operator_1>
```
子智能体向你提问
```
来自 Full-Stack Engineer_1 的提问
id: ask_fse_001
<Full-Stack Engineer_1>
<Ask>
我应该使用 JWT 还是 Session Cookie
</Ask>
</Full-Stack Engineer_1>
```
**回答提问**必须用 `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
<{你的显示名}>
<Output>
{你的输出}
</Output>
</{你的显示名}>
```
你不需要自己包裹 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"
)

View File

@ -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/<role_id>.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<body>.*?)\n---\s*\n(?P<rest>.*)$", 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"<RoleConfig id={self.role_id} name={self.name}>"

View File

@ -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 表示找到等待中的 futureFalse 表示无等待方或已超时
"""
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

View File

@ -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 bodyMarkdown"},
"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)

View File

@ -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":

View File

@ -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

View File

@ -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)

199
server/multi_agent.py Normal file
View File

@ -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"]

View File

@ -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 = '新对话';

View File

@ -6,6 +6,8 @@ export function dataState() {
// 路由相关
initialRouteResolved: false,
dropToolEvents: false,
// 多智能体模式开关
multiAgentMode: false,
// 轮询模式标志(禁用 WebSocket 事件处理)
usePollingMode: true,

View File

@ -38,6 +38,14 @@
宿主机模式免登录
</button>
<button
class="auth-secondary-button auth-tertiary-button"
@click="enterMultiAgent"
title="多智能体协作 beta"
>
多智能体模式beta
</button>
<div class="auth-error">{{ error }}</div>
<div class="auth-link">还没有账号<a href="/register">点击注册</a></div>
</section>
@ -96,6 +104,11 @@ const login = async () => {
}
};
const enterMultiAgent = () => {
// @login_required
window.location.href = '/multiagent/new';
};
const hostLogin = async () => {
hostSubmitting.value = true;
error.value = '';