agent-Specialization/modules/multi_agent/state.py
JOJO 811974d6e7 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);模块导入与功能断言测试全部通过。
2026-07-12 03:26:02 +08:00

339 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""多智能体会话状态机。
一个 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