refactor(multi-agent): 子智能体统一显示名寻址并增强可靠性
- 全局 agent_id 转为内部实现细节:由 next_free_agent_id 自动分配对话级 最小空闲正整数,不再接受模型指定、不出现在工具参数/结果文案中 - 角色内编号改为 peek + commit 两步:创建成功才消耗编号,失败不跳号 - 所有寻址类工具改用显示名:send_message/stop/terminate 传 display_name, get_sub_agent_status 传 display_names,sleep 的 wait_sub_agent_output, 子侧 ask_other_agent 传 target_display_name,answer_other_agent 去掉 source_agent_id - _run_loop 对 _call_model 增加重试:最多 5 次、间隔 10s、仅零接收时重试, 输出中断直接失败;多智能体模式下 5 次全失败转 idle 并向 Team Leader 报错 - 新增工具「正在调用」进度事件(calling),与 running/completed 共用 tool_call id,前端支持同 id 原地更新
This commit is contained in:
parent
4cf9c38137
commit
16aaea6b1e
@ -403,7 +403,8 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
### 11.1 角色与实例
|
||||
|
||||
- 主智能体显示名固定为 `Team Leader`,不需要专门的预置角色文件。
|
||||
- 子智能体 = `role_id`(如 `ui-operator` / `full-stack-engineer` / `code-reviewer` / `researcher`)+ `agent_id`(同一 role_id 下从 1 递增)。显示名格式 `{Role Name}_{agent_id}`,例如 `UI Operator_1`,后缀永远带数字。
|
||||
- 子智能体 = `role_id`(如 `ui-operator` / `full-stack-engineer` / `code-reviewer` / `researcher`)+ 角色内编号(同一 role_id 下从 1 递增)。显示名格式 `{Role Name}_{角色内编号}`,例如 `UI Operator_1`,后缀永远带数字。
|
||||
- **编号暴露原则(2026-08-26 起)**:角色内编号显示名是**唯一**对模型和用户暴露的身份;全局 `agent_id` 是纯内部实现细节(任务字典 key / task_id 生成),由系统自动分配对话级最小空闲正整数,不接受模型指定、不出现在工具参数/结果文案/前端展示中。所有寻址类工具(send_message/stop/terminate/get_sub_agent_status/sleep 的 wait_sub_agent_output、子侧 ask_other_agent)一律用显示名。
|
||||
- 主→子 / 子→主 / 子→子三种通信通过工具完成,工具签名见 `modules/multi_agent/tools.py`。
|
||||
- **`send_message_to_sub_agent` 与 `ask_sub_agent` 语义不同,必须保留两者**:前者插入引导消息不阻塞,后者阻塞等待一轮回答。
|
||||
- 子智能体间通信要求同时向主智能体输出汇报,不允许「偷偷沟通」。
|
||||
@ -411,6 +412,8 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
### 11.2 子智能体执行机制
|
||||
|
||||
- 子智能体在主进程内 `asyncio.Task`,跑在独立后台事件循环线程里(避开 Flask-SocketIO threading 冲突)。工具调用复用主进程沙箱/容器链路,网络调用走 `utils.api_client.APIClient`。
|
||||
- **模型请求重试(2026-08-26 起)**:`_run_loop` 对 `_call_model` 包重试循环,与主智能体 `run_streaming_attempts` 同构——最多 5 次尝试(`_SUB_AGENT_MAX_API_RETRIES=4`)、间隔 10s(`_SUB_AGENT_RETRY_DELAY_SECONDS`,用 `asyncio.sleep` 分段等待并响应软停止/取消)。重试条件:**仅当零接收**(未收到任何文本/思考/工具调用)才重试;已开始收到内容后断流(`SubAgentModelCallError.received_any=True`)直接失败。失败终态分模式:多智能体模式下 5 次全失败 → 转为 idle + `_forward_output_to_master` 报错(等 Team Leader 重新下达指令),输出期间断开 → 直接 failed 并同步向主智能体报错;传统模式一律 `_write_failure`。
|
||||
- **工具「正在调用」进度事件(2026-08-26 起)**:`_call_model` 在工具名+id 首个流式 chunk 到达时即 emit `status="calling"` 进度事件(与后续 running/completed 共用同一 tool_call id),前端按 id 原地更新条目;`RunnerDetailPanel.vue` / `SubAgentActivityDialog.vue` 的 normalizeStatus 识别 `calling`(显示 spinner + 「调用中」),并支持同一 id 历史条目跨组原地更新(多工具 calling 事件交错场景)。
|
||||
- 子智能体在多智能体模式下:
|
||||
- `create_sub_agent` 强制 `run_in_background=False`,不触发 `sub_agent_waiting` 事件,不阻塞前端输入区。
|
||||
- 子智能体自然的 assistant 输出结束(无 tool_calls)即本轮任务结束,进入 `idle`,上下文保留,不算 failed。
|
||||
@ -508,7 +511,7 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
|
||||
- 子智能体对话存在 `~/.astrion/astrion/host/host/data/sub_agents/`。重启后走 `manager.restore_sub_agent` 恢复实例引用。
|
||||
- **`MultiAgentState` 是进程级全局单例**(2026-07 重构):存放在 `modules/multi_agent/state.py` 的 `GLOBAL_MULTI_AGENT_STATES`(key=`conversation_id`),所有 `SubAgentManager` 实例共享;`manager.multi_agent_states` 只是该全局 dict 的引用。此前它是 manager 实例属性,对话级 terminal 缓存重建会产生多个 manager,各自的 `_load_state` 都从磁盘快照 `from_snapshot` 出一份独立副本,导致 terminate 只标记其中一份、前端轮询落到其他副本显示陈旧 idle。`get_or_create` / `drop` / `_load_state` restore 均通过 `GLOBAL_MULTI_AGENT_STATES_LOCK`(RLock)互斥。
|
||||
- **进程重启后的状态校准**(2026-07 新增):`_load_state` 恢复 ma 快照后,会用任务记录(持久真相)校准实例终态——任务记录是 terminated/终态而快照里还是 idle 的,一律校准为终态;同处还有存量 `_None` 后缀显示名的自愈迁移(按「对话×角色×创建时间」重编号)。
|
||||
- **显示名编号语义**:显示名后缀(如 `Full-Stack Engineer_1`)是**角色内编号**(`next_agent_id_for_role`,每次创建必递增),与内部 `agent_id`(LLM 可手动指定,如 10001)是两套独立命名空间,创建路径 `tools_execution.py` 中不得混用。
|
||||
- **显示名编号语义**:显示名后缀(如 `Full-Stack Engineer_1`)是**角色内编号**,创建路径 `tools_execution.py` 中通过 `peek_agent_id_for_role` + `commit_agent_id_for_role` 两步走——先 peek 构造显示名,创建成功后才提交计数器,**失败不消耗编号**(避免跳号);全局 `agent_id` 由 `manager.next_free_agent_id()` 自动分配(对话级最小空闲正整数),两者是两套独立命名空间,不得混用。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -99,7 +99,7 @@ class ToolsDefinitionCoreToolsMixin:
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "sleep",
|
||||
"description": "等待工具。三种模式三选一:1) seconds:短暂延迟;2) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果;3) wait_sub_agent_output_ids:等待指定子智能体下一次输出并直接返回该消息(多智能体模式专用)。若同时提供多个等待参数会报错。",
|
||||
"description": "等待工具。三种模式三选一:1) seconds:短暂延迟;2) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果;3) wait_sub_agent_output:等待指定子智能体下一次输出并直接返回该消息(多智能体模式专用)。若同时提供多个等待参数会报错。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": self._inject_intent({
|
||||
@ -111,10 +111,9 @@ class ToolsDefinitionCoreToolsMixin:
|
||||
"type": "string",
|
||||
"description": "等待指定后台 run_command 的 command_id 结束后返回。"
|
||||
},
|
||||
"wait_sub_agent_output_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"description": "等待指定子智能体下一次输出并直接返回该消息。多智能体模式专用,且一次只能包含一个编号。"
|
||||
"wait_sub_agent_output": {
|
||||
"type": "string",
|
||||
"description": "等待指定子智能体下一次输出并直接返回该消息。多智能体模式专用,传子智能体显示名(如 UI Operator_1)。"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
|
||||
@ -170,7 +170,7 @@ class ToolsDefinitionTerminalToolsMixin:
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_command",
|
||||
"description": "执行一次性终端命令,适合查看文件信息(file/ls/stat/iconv 等)、转换编码或调用 CLI 工具。禁止启动交互式程序。必须提供 timeout。前台模式(run_in_background=false,默认)上限120秒,超时会打断;后台模式(run_in_background=true)上限3600秒,会先等待5秒返回已有输出并继续在后台运行,完成后由系统通知。",
|
||||
"description": "执行一次性终端命令,适合查看文件信息(file/ls/stat/iconv 等)、转换编码或调用 CLI 工具。禁止启动交互式程序。必须提供 timeout。前台模式(run_in_background=false,默认)上限120秒,超时会打断;后台模式(run_in_background=true)上限3600秒,会先等待5秒返回已有输出并继续在后台运行,完成后由系统通知。后台模式会返回所有指令输出结果,禁止用于启动后台服务。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": self._inject_intent({
|
||||
@ -181,7 +181,7 @@ class ToolsDefinitionTerminalToolsMixin:
|
||||
},
|
||||
"run_in_background": {
|
||||
"type": "boolean",
|
||||
"description": "是否后台运行。true 时先等待5秒返回已有输出,并在后台持续执行直至结束。"
|
||||
"description": "是否后台运行。true 时先等待5秒返回已有输出,并在后台持续执行直至结束。禁止用于启动后台服务(如常驻服务进程、watch 类命令)。"
|
||||
}
|
||||
}),
|
||||
"required": ["command", "timeout"]
|
||||
|
||||
@ -1382,7 +1382,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
elif tool_name == "sleep":
|
||||
seconds = arguments.get("seconds")
|
||||
wait_sub_agent_ids = arguments.get("wait_sub_agent_ids")
|
||||
wait_sub_agent_output_ids = arguments.get("wait_sub_agent_output_ids")
|
||||
wait_sub_agent_output = arguments.get("wait_sub_agent_output")
|
||||
wait_runcommand_id = arguments.get("wait_runcommand_id")
|
||||
reason = arguments.get("reason", "等待操作完成")
|
||||
|
||||
@ -1391,67 +1391,65 @@ class MainTerminalToolsExecutionMixin:
|
||||
provided += 1
|
||||
if wait_sub_agent_ids:
|
||||
provided += 1
|
||||
if wait_sub_agent_output_ids:
|
||||
if wait_sub_agent_output:
|
||||
provided += 1
|
||||
if wait_runcommand_id:
|
||||
provided += 1
|
||||
if provided == 0:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "sleep 至少需要提供一个参数:seconds / wait_sub_agent_ids / wait_sub_agent_output_ids / wait_runcommand_id"
|
||||
"error": "sleep 至少需要提供一个参数:seconds / wait_sub_agent_ids / wait_sub_agent_output / wait_runcommand_id"
|
||||
}
|
||||
elif provided > 1:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "sleep 的等待参数互斥:seconds / wait_sub_agent_ids / wait_sub_agent_output_ids / wait_runcommand_id 只能提供一个"
|
||||
"error": "sleep 的等待参数互斥:seconds / wait_sub_agent_ids / wait_sub_agent_output / wait_runcommand_id 只能提供一个"
|
||||
}
|
||||
elif wait_sub_agent_output_ids:
|
||||
elif wait_sub_agent_output:
|
||||
if not getattr(self, "multi_agent_mode", False):
|
||||
result = {"success": False, "error": "wait_sub_agent_output_ids 仅在多智能体模式下可用"}
|
||||
elif not isinstance(wait_sub_agent_output_ids, list) or len(wait_sub_agent_output_ids) != 1:
|
||||
result = {"success": False, "error": "wait_sub_agent_output_ids 必须包含且仅包含一个子智能体编号"}
|
||||
result = {"success": False, "error": "wait_sub_agent_output 仅在多智能体模式下可用"}
|
||||
else:
|
||||
try:
|
||||
agent_id = int(wait_sub_agent_output_ids[0])
|
||||
if agent_id <= 0:
|
||||
raise ValueError()
|
||||
except Exception:
|
||||
result = {"success": False, "error": "wait_sub_agent_output_ids 必须是正整数"}
|
||||
display_name = str(wait_sub_agent_output or "").strip()
|
||||
manager = getattr(self, "sub_agent_manager", None)
|
||||
if not manager:
|
||||
result = {"success": False, "error": "子智能体管理器不可用"}
|
||||
else:
|
||||
manager = getattr(self, "sub_agent_manager", None)
|
||||
if not manager:
|
||||
result = {"success": False, "error": "子智能体管理器不可用"}
|
||||
state = manager.get_multi_agent_state(getattr(self.context_manager, "current_conversation_id", None))
|
||||
if not state:
|
||||
result = {"success": False, "error": "当前对话没有多智能体状态"}
|
||||
else:
|
||||
state = manager.get_multi_agent_state(getattr(self.context_manager, "current_conversation_id", None))
|
||||
if not state:
|
||||
result = {"success": False, "error": "当前对话没有多智能体状态"}
|
||||
# 显示名寻址:模型只传显示名,内部解析为全局 agent_id
|
||||
inst = state.get_instance_by_display_name(display_name)
|
||||
if not inst:
|
||||
available = "、".join(state.list_display_names()) or "(无)"
|
||||
result = {"success": False, "error": f"未找到子智能体「{display_name}」。当前已有: {available}"}
|
||||
else:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = state.register_output_wait(agent_id, loop)
|
||||
fut = state.register_output_wait(inst.agent_id, loop)
|
||||
msg = await asyncio.wait_for(fut, timeout=300)
|
||||
result = {
|
||||
"success": True,
|
||||
"mode": "wait_sub_agent_output",
|
||||
"agent_id": agent_id,
|
||||
"display_name": inst.display_name,
|
||||
"message": msg,
|
||||
}
|
||||
except asyncio.TimeoutError:
|
||||
result = {"success": False, "error": f"等待子智能体 {agent_id} 输出超时(5分钟)。可用 send_message_to_sub_agent 重新激活。"}
|
||||
result = {"success": False, "error": f"等待 {inst.display_name} 输出超时(5分钟)。可用 send_message_to_sub_agent 重新激活。"}
|
||||
except asyncio.CancelledError:
|
||||
result = {"success": False, "error": f"等待子智能体 {agent_id} 被取消。该子智能体现在不可用,可用 send_message_to_sub_agent 重新激活。"}
|
||||
result = {"success": False, "error": f"等待 {inst.display_name} 被取消。该子智能体现在不可用,可用 send_message_to_sub_agent 重新激活。"}
|
||||
except RuntimeError as exc:
|
||||
error_msg = str(exc)
|
||||
if "send_message_to_sub_agent" not in error_msg:
|
||||
error_msg += " 可用 send_message_to_sub_agent 重新激活。"
|
||||
result = {"success": False, "error": error_msg}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": f"等待子智能体 {agent_id} 输出失败: {exc}。可用 send_message_to_sub_agent 重新激活。"}
|
||||
result = {"success": False, "error": f"等待 {inst.display_name} 输出失败: {exc}。可用 send_message_to_sub_agent 重新激活。"}
|
||||
elif wait_sub_agent_ids:
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "多智能体模式下 sleep 工具不支持 wait_sub_agent_ids,请使用 wait_sub_agent_output_ids。"
|
||||
"error": "多智能体模式下 sleep 工具不支持 wait_sub_agent_ids,请使用 wait_sub_agent_output。"
|
||||
}
|
||||
elif not isinstance(wait_sub_agent_ids, list) or not wait_sub_agent_ids:
|
||||
result = {"success": False, "error": "wait_sub_agent_ids 必须是非空数组"}
|
||||
@ -2152,14 +2150,15 @@ class MainTerminalToolsExecutionMixin:
|
||||
else:
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
multi_agent_state = self.sub_agent_manager.get_or_create_multi_agent_state(conv_id)
|
||||
# 角色内编号:每次创建都递增,作为显示名后缀
|
||||
# (如 Full-Stack Engineer_1)。它与内部 agent_id 是两套
|
||||
# 独立命名空间——agent_id 可被 LLM 手动指定(如 10001),
|
||||
# 显示名后缀永远用角色内编号。
|
||||
role_seq = multi_agent_state.next_agent_id_for_role(role_id)
|
||||
agent_id = arguments.get("agent_id")
|
||||
if not agent_id:
|
||||
agent_id = role_seq
|
||||
# 角色内编号:显示名后缀(如 UI Operator_1),是唯一对模型/用户
|
||||
# 暴露的编号。采用 peek + commit:先预取构造显示名,创建成功后才
|
||||
# 提交计数器,失败不消耗编号,避免跳号。
|
||||
role_seq = multi_agent_state.peek_agent_id_for_role(role_id)
|
||||
# 全局 agent_id 是纯内部实现细节(任务字典 key / task_id 生成),
|
||||
# 不暴露给模型与用户,也不接受模型指定:自动分配对话级最小空闲正整数。
|
||||
agent_id = self.sub_agent_manager.next_free_agent_id(
|
||||
conv_id, extra_used=set(multi_agent_state.agents.keys())
|
||||
)
|
||||
# 构造显示名
|
||||
display_name = role.display_name(int(role_seq))
|
||||
# 构造多智能体版系统提示词(含动态上下文注入)
|
||||
@ -2194,7 +2193,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
pass
|
||||
# 走原行 发事件创建(避免后期重建提供重复工能重费,直接使用 multi_agent_mode=True 调用)
|
||||
result = self.sub_agent_manager.create_sub_agent(
|
||||
agent_id=int(agent_id),
|
||||
agent_id=agent_id,
|
||||
summary=summary_text,
|
||||
task=arguments.get("task", ""),
|
||||
run_in_background=False,
|
||||
@ -2211,6 +2210,9 @@ class MainTerminalToolsExecutionMixin:
|
||||
)
|
||||
# 在多智能体模式下,子智能体是团队协作成员,不是传统后台任务。
|
||||
# run_in_background=False 避免触发后台完成通知轮询,保持主对话输入区可用。
|
||||
if result.get("success"):
|
||||
# 创建成功才提交角色内编号;失败时 peek 未提交,编号不被消耗
|
||||
multi_agent_state.commit_agent_id_for_role(role_id, role_seq)
|
||||
except Exception as exc:
|
||||
logger.exception("[multi_agent] create_sub_agent failed")
|
||||
result = {"success": False, "error": str(exc)}
|
||||
@ -2277,29 +2279,60 @@ class MainTerminalToolsExecutionMixin:
|
||||
pass
|
||||
|
||||
elif tool_name == "terminate_sub_agent":
|
||||
result = self.sub_agent_manager.terminate_sub_agent(
|
||||
agent_id=arguments.get("agent_id")
|
||||
)
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
# 多智能体模式:按显示名寻址,内部解析为全局 agent_id
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
display_name = str(arguments.get("display_name") or "").strip()
|
||||
inst = state.get_instance_by_display_name(display_name) if state else None
|
||||
if not inst:
|
||||
available = "、".join(state.list_display_names()) if state else ""
|
||||
result = {"success": False, "error": f"未找到子智能体「{display_name}」。当前已有: {available or '(无)'}"}
|
||||
else:
|
||||
result = self.sub_agent_manager.terminate_sub_agent(agent_id=inst.agent_id)
|
||||
if isinstance(result, dict):
|
||||
result["display_name"] = inst.display_name
|
||||
state.mark_status(inst.agent_id, "terminated")
|
||||
else:
|
||||
result = self.sub_agent_manager.terminate_sub_agent(
|
||||
agent_id=arguments.get("agent_id")
|
||||
)
|
||||
# 主智能体主动终结时,tool 结果(message)已包含结论,
|
||||
# 摘掉 system_message 避免工具循环再注入一条冗余 user 消息
|
||||
# (且「已被手动关闭」措辞对此场景是误导)。
|
||||
# 前端 UI 手动终止走 server/conversation.py API 直调 manager,不受影响。
|
||||
if isinstance(result, dict):
|
||||
result.pop("system_message", None)
|
||||
# 多智能体模式:同步状态到 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", [])
|
||||
)
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
# 多智能体模式:按显示名列表查询,内部解析为全局 agent_id
|
||||
names = arguments.get("display_names")
|
||||
if not isinstance(names, list) or not names:
|
||||
result = {"success": False, "error": "display_names 必须是非空数组(子智能体显示名,如 UI Operator_1)"}
|
||||
else:
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
agent_ids = []
|
||||
not_found_names = []
|
||||
for name in names:
|
||||
inst = state.get_instance_by_display_name(str(name)) if state else None
|
||||
if inst:
|
||||
agent_ids.append(inst.agent_id)
|
||||
else:
|
||||
not_found_names.append(str(name))
|
||||
results = []
|
||||
if agent_ids:
|
||||
# agent_ids 非空时 manager 侧总是返回 success=True
|
||||
r = self.sub_agent_manager.get_sub_agent_status(agent_ids=agent_ids)
|
||||
results.extend(r.get("results") or [])
|
||||
for name in not_found_names:
|
||||
results.append({"found": False, "display_name": name, "error": "子智能体不存在"})
|
||||
result = {"success": True, "results": results}
|
||||
else:
|
||||
result = self.sub_agent_manager.get_sub_agent_status(
|
||||
agent_ids=arguments.get("agent_ids", [])
|
||||
)
|
||||
|
||||
# 多智能体模式专属工具:send_message_to_sub_agent / stop_sub_agent / answer_sub_agent_question / create_custom_agent / list_agents / list_active_sub_agents
|
||||
elif tool_name == "send_message_to_sub_agent":
|
||||
@ -2308,38 +2341,47 @@ class MainTerminalToolsExecutionMixin:
|
||||
else:
|
||||
try:
|
||||
from modules.multi_agent.state import build_master_message_to_sub_agent
|
||||
agent_id = int(arguments.get("agent_id", 0))
|
||||
display_name = str(arguments.get("display_name") or "").strip()
|
||||
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)
|
||||
ma_debug(
|
||||
"tool_send_message_to_sub_agent",
|
||||
agent_id=agent_id,
|
||||
raw_message=str(message)[:500],
|
||||
wrapped_message_preview=text[:500],
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
||||
if not ok:
|
||||
latest = self.sub_agent_manager._latest_task_for_agent(agent_id)
|
||||
if latest and latest.get("status") == "terminated":
|
||||
result = {"success": False, "error": f"子智能体 {agent_id} 已被手动终结,无法再接收消息。如需继续工作,请创建新的子智能体。"}
|
||||
else:
|
||||
result = {"success": False, "error": f"子智能体 {agent_id} 不存在或已结束"}
|
||||
# 显示名寻址:模型只知道角色内编号显示名(如 UI Operator_1),
|
||||
# 全局 agent_id 在内部解析,不暴露给模型
|
||||
inst = state.get_instance_by_display_name(display_name)
|
||||
if not inst:
|
||||
available = "、".join(state.list_display_names()) or "(无)"
|
||||
result = {"success": False, "error": f"未找到子智能体「{display_name}」。当前已有: {available}"}
|
||||
else:
|
||||
result = {"success": True, "agent_id": agent_id}
|
||||
ma_debug(
|
||||
"tool_send_message_to_sub_agent_result",
|
||||
agent_id=agent_id,
|
||||
conversation_id=conv_id,
|
||||
ok=ok,
|
||||
result=result,
|
||||
)
|
||||
agent_id = inst.agent_id
|
||||
# 构造消息文本并插入子对话
|
||||
text = build_master_message_to_sub_agent(message)
|
||||
ma_debug(
|
||||
"tool_send_message_to_sub_agent",
|
||||
agent_id=agent_id,
|
||||
display_name=display_name,
|
||||
raw_message=str(message)[:500],
|
||||
wrapped_message_preview=text[:500],
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
||||
if not ok:
|
||||
latest = self.sub_agent_manager._latest_task_for_agent(agent_id)
|
||||
if latest and latest.get("status") == "terminated":
|
||||
result = {"success": False, "error": f"{display_name} 已被手动终结,无法再接收消息。如需继续工作,请创建新的子智能体。"}
|
||||
else:
|
||||
result = {"success": False, "error": f"{display_name} 不存在或已结束"}
|
||||
else:
|
||||
result = {"success": True, "display_name": display_name}
|
||||
ma_debug(
|
||||
"tool_send_message_to_sub_agent_result",
|
||||
agent_id=agent_id,
|
||||
conversation_id=conv_id,
|
||||
ok=ok,
|
||||
result=result,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("[multi_agent] send_message_to_sub_agent failed")
|
||||
result = {"success": False, "error": str(exc)}
|
||||
@ -2349,8 +2391,17 @@ class MainTerminalToolsExecutionMixin:
|
||||
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
|
||||
else:
|
||||
try:
|
||||
agent_id = int(arguments.get("agent_id", 0))
|
||||
result = self.sub_agent_manager.stop_sub_agent(agent_id=agent_id)
|
||||
display_name = str(arguments.get("display_name") or "").strip()
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
inst = state.get_instance_by_display_name(display_name) if state else None
|
||||
if not inst:
|
||||
available = "、".join(state.list_display_names()) if state else ""
|
||||
result = {"success": False, "error": f"未找到子智能体「{display_name}」。当前已有: {available or '(无)'}"}
|
||||
else:
|
||||
result = self.sub_agent_manager.stop_sub_agent(agent_id=inst.agent_id)
|
||||
if isinstance(result, dict):
|
||||
result["display_name"] = inst.display_name
|
||||
except Exception as exc:
|
||||
logger.exception("[multi_agent] stop_sub_agent failed")
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
@ -287,19 +287,46 @@ class MultiAgentState:
|
||||
# 一个 agent 可能同时只阻塞在一个 ask 工具上(最简实现)
|
||||
# key = agent_id, value = question_id(表示当前 agent 正阻塞等待)
|
||||
self.agent_blocking_question: Dict[int, str] = {}
|
||||
# 主智能体通过 sleep(wait_sub_agent_output_ids) 等待某个子智能体下一次输出
|
||||
# 主智能体通过 sleep(wait_sub_agent_output) 等待某个子智能体下一次输出
|
||||
# key = agent_id, value = asyncio.Future(结果为完整消息文本)
|
||||
self.output_waits: Dict[int, asyncio.Future] = {}
|
||||
# 角色实例计数:role_id -> 已分配的最大 agent_id(数字)
|
||||
# 用于创建新实例时自动递增编号,但允许调用方显式指定
|
||||
# 角色实例计数:role_id -> 已成功创建实例使用的最大角色内编号
|
||||
# 角色内编号是显示名后缀(如 UI Operator_1),也是唯一对模型/用户暴露的编号;
|
||||
# 全局 agent_id 为内部实现细节,不对外暴露。
|
||||
# 采用 peek + commit 两步:创建失败不消耗编号,避免跳号。
|
||||
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 peek_agent_id_for_role(self, role_id: str) -> int:
|
||||
"""预取指定角色的下一个角色内编号(不递增计数器)。
|
||||
|
||||
创建子智能体时先 peek 构造显示名,创建成功后必须调
|
||||
commit_agent_id_for_role 提交;失败则不提交,编号不被消耗。
|
||||
"""
|
||||
return self.role_counters.get(role_id, 0) + 1
|
||||
|
||||
def commit_agent_id_for_role(self, role_id: str, seq: int) -> None:
|
||||
"""创建成功后提交角色内编号(单调递增,不回退)。"""
|
||||
if seq > self.role_counters.get(role_id, 0):
|
||||
self.role_counters[role_id] = seq
|
||||
|
||||
def get_instance_by_display_name(self, display_name: str) -> Optional[AgentInstance]:
|
||||
"""按显示名(如 UI Operator_1)查找实例;精确匹配优先,大小写不敏感兜底。"""
|
||||
name = (display_name or "").strip()
|
||||
if not name:
|
||||
return None
|
||||
for a in self.agents.values():
|
||||
if a.display_name == name:
|
||||
return a
|
||||
lowered = name.lower()
|
||||
for a in self.agents.values():
|
||||
if a.display_name.lower() == lowered:
|
||||
return a
|
||||
return None
|
||||
|
||||
def list_display_names(self) -> List[str]:
|
||||
"""返回当前所有实例的显示名(用于错误提示)。"""
|
||||
return [a.display_name for a in self.agents.values()]
|
||||
|
||||
def register_instance(self, instance: AgentInstance) -> None:
|
||||
if instance.agent_id in self.agents:
|
||||
@ -339,7 +366,7 @@ class MultiAgentState:
|
||||
a.status = status
|
||||
if last_output:
|
||||
a.last_output = last_output
|
||||
# 当子智能体进入终态/idle 时,立即取消 sleep(wait_sub_agent_output_ids) 的等待
|
||||
# 当子智能体进入终态/idle 时,立即取消 sleep(wait_sub_agent_output) 的等待
|
||||
if status in ("failed", "terminated", "idle"):
|
||||
self._cancel_output_wait(agent_id, status)
|
||||
|
||||
@ -348,12 +375,14 @@ class MultiAgentState:
|
||||
fut = self.output_waits.pop(agent_id, None)
|
||||
if not fut or fut.done():
|
||||
return
|
||||
inst = self.agents.get(agent_id)
|
||||
name = inst.display_name if inst else str(agent_id)
|
||||
if status == "terminated":
|
||||
msg = f"子智能体 {agent_id} 已终止,无法继续等待输出。"
|
||||
msg = f"子智能体 {name} 已终止,无法继续等待输出。"
|
||||
elif status == "failed":
|
||||
msg = f"子智能体 {agent_id} 已失败,无法继续等待输出。该子智能体可被复活,可用 send_message_to_sub_agent 重新激活。"
|
||||
msg = f"子智能体 {name} 已失败,无法继续等待输出。该子智能体可被复活,可用 send_message_to_sub_agent 重新激活。"
|
||||
else:
|
||||
msg = f"子智能体 {agent_id} 已进入空闲状态,无法继续等待输出。可用 send_message_to_sub_agent 重新激活。"
|
||||
msg = f"子智能体 {name} 已进入空闲状态,无法继续等待输出。可用 send_message_to_sub_agent 重新激活。"
|
||||
try:
|
||||
loop = fut.get_loop()
|
||||
if loop and not loop.is_closed():
|
||||
@ -394,7 +423,7 @@ class MultiAgentState:
|
||||
def has_pending_master_messages(self) -> bool:
|
||||
return len(self.pending_master_messages) > 0
|
||||
|
||||
# ----- 等待子智能体输出(sleep wait_sub_agent_output_ids) -----
|
||||
# ----- 等待子智能体输出(sleep wait_sub_agent_output) -----
|
||||
def register_output_wait(
|
||||
self, agent_id: int, loop: AbstractEventLoop
|
||||
) -> asyncio.Future:
|
||||
@ -406,13 +435,13 @@ class MultiAgentState:
|
||||
fut: asyncio.Future = loop.create_future()
|
||||
inst = self.agents.get(agent_id)
|
||||
if not inst:
|
||||
fut.set_exception(ValueError(f"未找到子智能体 {agent_id}"))
|
||||
fut.set_exception(ValueError("未找到该子智能体"))
|
||||
return fut
|
||||
|
||||
if inst.status in ("terminated", "failed"):
|
||||
fut.set_exception(
|
||||
RuntimeError(
|
||||
f"子智能体 {agent_id} 当前状态为 {inst.status},无法等待输出"
|
||||
f"子智能体 {inst.display_name} 当前状态为 {inst.status},无法等待输出"
|
||||
)
|
||||
)
|
||||
return fut
|
||||
|
||||
@ -49,10 +49,6 @@ def _master_tool_create_sub_agent() -> Dict[str, Any]:
|
||||
"type": "string",
|
||||
"description": "要交给该子智能体执行的任务描述。要求包含:目标、范围、产出、注意事项。",
|
||||
},
|
||||
"agent_id": {
|
||||
"type": "integer",
|
||||
"description": "(可选)手动指定实例编号;不传时自动递增。",
|
||||
},
|
||||
"thinking_mode": {
|
||||
"type": "string",
|
||||
"enum": ["fast", "thinking"],
|
||||
@ -74,9 +70,9 @@ def _master_tool_stop_sub_agent() -> Dict[str, Any]:
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "要暂停的子智能体编号。"},
|
||||
"display_name": {"type": "string", "description": "要暂停的子智能体显示名(如 UI Operator_1)。"},
|
||||
}),
|
||||
"required": ["agent_id"],
|
||||
"required": ["display_name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -91,9 +87,9 @@ def _master_tool_terminate_sub_agent() -> Dict[str, Any]:
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "要终止的子智能体编号。"},
|
||||
"display_name": {"type": "string", "description": "要终止的子智能体显示名(如 UI Operator_1)。"},
|
||||
}),
|
||||
"required": ["agent_id"],
|
||||
"required": ["display_name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -112,10 +108,10 @@ def _master_tool_send_message_to_sub_agent() -> Dict[str, Any]:
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "目标子智能体编号。"},
|
||||
"display_name": {"type": "string", "description": "目标子智能体显示名(如 UI Operator_1)。"},
|
||||
"message": {"type": "string", "description": "要插入的消息或新任务正文。"},
|
||||
}),
|
||||
"required": ["agent_id", "message"],
|
||||
"required": ["display_name", "message"],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -183,7 +179,7 @@ def _master_tool_list_active_sub_agents() -> Dict[str, Any]:
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_active_sub_agents",
|
||||
"description": "列出当前多智能体会话中所有活跃/已创建的子智能体实例(agent_id/role/display_name/status)。",
|
||||
"description": "列出当前多智能体会话中所有已创建的子智能体实例(显示名/角色/状态/任务摘要)。",
|
||||
"parameters": {"type": "object", "properties": _inject_intent({})},
|
||||
},
|
||||
}
|
||||
@ -198,9 +194,9 @@ def _master_tool_get_sub_agent_status() -> Dict[str, Any]:
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_ids": {"type": "array", "items": {"type": "integer"}, "description": "要查询的子智能体编号列表。"},
|
||||
"display_names": {"type": "array", "items": {"type": "string"}, "description": "要查询的子智能体显示名列表(如 [\"UI Operator_1\", \"Researcher_2\"])。"},
|
||||
}),
|
||||
"required": ["agent_ids"],
|
||||
"required": ["display_names"],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -254,11 +250,11 @@ def _sub_tool_ask_other_agent() -> Dict[str, Any]:
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"target_agent_id": {"type": "integer", "description": "目标子智能体编号。"},
|
||||
"target_display_name": {"type": "string", "description": "目标子智能体显示名(如 Researcher_2),可先用 list_active_sub_agents 查看。"},
|
||||
"question": {"type": "string", "description": "提问内容。"},
|
||||
"question_id": {"type": "string", "description": "(可选)问题 id。"},
|
||||
}),
|
||||
"required": ["target_agent_id", "question"],
|
||||
"required": ["target_display_name", "question"],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -276,11 +272,10 @@ def _sub_tool_answer_other_agent() -> Dict[str, Any]:
|
||||
"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"],
|
||||
"required": ["question_id", "answer"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
from config import SUB_AGENT_DEFAULT_TIMEOUT, SUB_AGENT_MAX_ACTIVE
|
||||
|
||||
@ -53,6 +53,36 @@ class SubAgentCreationMixin:
|
||||
used = self.conversation_agents.setdefault(conversation_id, [])
|
||||
return agent_id not in used
|
||||
|
||||
def next_free_agent_id(self, conversation_id: Optional[str], extra_used: Optional[Set[int]] = None) -> int:
|
||||
"""分配对话级全局最小空闲 agent_id。
|
||||
|
||||
agent_id 是对话全局唯一的内部编号(不对外暴露:模型与用户只看到
|
||||
角色内编号显示名)。多智能体模式下创建实例时由系统自动分配,
|
||||
不接受调用方指定。
|
||||
"""
|
||||
used: Set[int] = set()
|
||||
if conversation_id:
|
||||
for aid in self.conversation_agents.get(conversation_id, []) or []:
|
||||
try:
|
||||
used.add(int(aid))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
for task in self.tasks.values():
|
||||
if conversation_id and task.get("conversation_id") != conversation_id:
|
||||
continue
|
||||
try:
|
||||
aid = task.get("agent_id")
|
||||
if aid is not None:
|
||||
used.add(int(aid))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if extra_used:
|
||||
used.update(extra_used)
|
||||
n = 1
|
||||
while n in used:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
def _mark_agent_id_used(self, conversation_id: str, agent_id: int):
|
||||
used = self.conversation_agents.setdefault(conversation_id, [])
|
||||
if agent_id not in used:
|
||||
|
||||
@ -260,6 +260,13 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
return {"success": False, "error": "缺少对话ID,无法创建子智能体"}
|
||||
|
||||
if not self._ensure_agent_slot_available(conversation_id, agent_id):
|
||||
# 多智能体模式的 agent_id 由系统自动分配且不对外暴露,
|
||||
# 走到这里说明自动分配与其他写入路径竞争出错,不应把内部编号抛给模型
|
||||
if multi_agent_mode:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "内部错误:实例编号分配冲突,请重试创建。"
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"该对话已使用过编号 {agent_id},请更换新的子智能体代号。"
|
||||
@ -354,7 +361,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
multi_agent_state.register_instance(inst)
|
||||
except ValueError:
|
||||
shutil.rmtree(task_root, ignore_errors=True)
|
||||
return {"success": False, "error": f"agent_id {agent_id} 已在该会话中使用"}
|
||||
# 多智能体模式的 agent_id 是内部编号,不把具体值抛给模型
|
||||
return {"success": False, "error": "内部错误:实例注册冲突,请重试创建。"}
|
||||
|
||||
sub_agent = SubAgentTask(
|
||||
manager=self,
|
||||
@ -427,7 +435,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
|
||||
message = f"子智能体{agent_id} 已创建,任务ID: {task_id}"
|
||||
if multi_agent_mode and display_name:
|
||||
message = f"{display_name} 已创建,任务ID: {task_id}"
|
||||
# 多智能体模式:对用户/模型只暴露显示名,task_id 为内部细节不进文案
|
||||
message = f"{display_name} 已创建。"
|
||||
print(f"{OUTPUT_FORMATS['info']} {message}")
|
||||
ma_debug(
|
||||
"manager_create_sub_agent",
|
||||
@ -607,11 +616,13 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
agent_id=real_agent_id,
|
||||
had_instance=bool(sub_agent),
|
||||
)
|
||||
display_name = task.get("display_name") or f"子智能体{real_agent_id}"
|
||||
return {
|
||||
"success": True,
|
||||
"task_id": real_task_id,
|
||||
"agent_id": real_agent_id,
|
||||
"message": f"子智能体{real_agent_id} 已暂停,可用 send_message_to_sub_agent 重新激活。",
|
||||
"display_name": task.get("display_name") or None,
|
||||
"message": f"{display_name} 已暂停,可用 send_message_to_sub_agent 重新激活。",
|
||||
}
|
||||
|
||||
def terminate_sub_agent(
|
||||
@ -853,6 +864,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
"task_id": task["task_id"],
|
||||
"status": status,
|
||||
"summary": task.get("summary"),
|
||||
"display_name": task.get("display_name"),
|
||||
"created_at": task.get("created_at"),
|
||||
"updated_at": task.get("updated_at"),
|
||||
"deliverables_dir": task.get("deliverables_dir"),
|
||||
|
||||
@ -31,6 +31,26 @@ from modules.multi_agent.debug_logger import ma_debug
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
class SubAgentModelCallError(RuntimeError):
|
||||
"""子智能体模型请求失败。
|
||||
|
||||
received_any=False:请求阶段失败(未收到任何文本/思考/工具调用),可重试;
|
||||
received_any=True:已开始收到流内容后断开(输出期间断开),直接失败不重试。
|
||||
语义与主智能体 run_streaming_attempts 的 can_retry 判定一致
|
||||
(not full_response and not tool_calls and not current_thinking)。
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, *, received_any: bool = False):
|
||||
super().__init__(message)
|
||||
self.received_any = received_any
|
||||
|
||||
|
||||
# 模型请求失败重试策略(与主智能体 max_api_retries=4 / retry_delay_seconds=10 对齐:
|
||||
# 最多重试 4 次,即共 5 次尝试,重试间隔 10 秒)
|
||||
_SUB_AGENT_MAX_API_RETRIES = 4
|
||||
_SUB_AGENT_RETRY_DELAY_SECONDS = 10
|
||||
|
||||
# 多智能体模式下额外加载的工具定义
|
||||
def _load_multi_agent_sub_agent_tools() -> List[Dict[str, Any]]:
|
||||
try:
|
||||
@ -310,7 +330,80 @@ class SubAgentTask:
|
||||
# 安全点:写入延迟的上下文通知(不触发新一轮工作,仅让下一轮模型调用看到)
|
||||
self._flush_pending_notifications()
|
||||
|
||||
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
||||
# 模型请求(带失败重试,与主智能体同构:最多 5 次尝试、重试间隔 10s、
|
||||
# 仅当零接收——未收到任何文本/思考/工具调用——时重试;
|
||||
# 已开始收到内容后断开属于输出期间故障,直接失败不重试)
|
||||
assistant_message = ""
|
||||
reasoning = ""
|
||||
tool_calls = []
|
||||
usage = None
|
||||
call_error: Optional[SubAgentModelCallError] = None
|
||||
for api_attempt in range(_SUB_AGENT_MAX_API_RETRIES + 1):
|
||||
if self._cancelled:
|
||||
raise asyncio.CancelledError()
|
||||
try:
|
||||
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
||||
call_error = None
|
||||
break
|
||||
except SubAgentModelCallError as exc:
|
||||
call_error = exc
|
||||
can_retry = api_attempt < _SUB_AGENT_MAX_API_RETRIES and not exc.received_any
|
||||
ma_debug(
|
||||
"sub_agent_api_attempt_failed",
|
||||
task_id=self.task_id,
|
||||
agent_id=self.agent_id,
|
||||
display_name=self.display_name,
|
||||
turn=turn,
|
||||
attempt=api_attempt + 1,
|
||||
max_attempts=_SUB_AGENT_MAX_API_RETRIES + 1,
|
||||
received_any=exc.received_any,
|
||||
can_retry=can_retry,
|
||||
error=str(exc)[:500],
|
||||
)
|
||||
if not can_retry:
|
||||
break
|
||||
# 重试本身也是一次额外 API 请求
|
||||
self.stats["api_calls"] += 1
|
||||
# 重试等待期间必须响应软停止/硬取消
|
||||
wait_until = time.time() + _SUB_AGENT_RETRY_DELAY_SECONDS
|
||||
while time.time() < wait_until:
|
||||
if self._cancelled:
|
||||
raise asyncio.CancelledError()
|
||||
if self._soft_stop:
|
||||
break
|
||||
await asyncio.sleep(0.2)
|
||||
continue
|
||||
|
||||
if call_error is not None:
|
||||
if self.multi_agent_mode and not call_error.received_any:
|
||||
# 5 次尝试全部失败(均为零接收):子智能体不终止,转为 idle
|
||||
# 并向 Team Leader 汇报错误,等待检查网络后重新下达指令
|
||||
error_report = (
|
||||
f"⚠️ 模型请求连续 {_SUB_AGENT_MAX_API_RETRIES + 1} 次失败(网络或 API 异常):"
|
||||
f"{call_error}。本轮任务无法继续,我已进入空闲状态,请检查网络/模型服务后重新给我下达指令。"
|
||||
)
|
||||
ma_debug(
|
||||
"sub_agent_api_retries_exhausted_idle",
|
||||
task_id=self.task_id,
|
||||
agent_id=self.agent_id,
|
||||
display_name=self.display_name,
|
||||
turn=turn,
|
||||
)
|
||||
self._forward_output_to_master(error_report, is_final=True)
|
||||
self._mark_idle()
|
||||
self._idle = True
|
||||
self._persist_conversation(partial_summary=error_report[:200])
|
||||
continue
|
||||
# 输出期间断开(已开始收到内容)或传统后台模式:直接失败,
|
||||
# 异常上抛由 run() 捕获后 _write_failure 落盘 failed 状态
|
||||
if self.multi_agent_mode:
|
||||
# 同步把失败状态告知 Team Leader,避免主智能体无感知空等
|
||||
self._forward_output_to_master(
|
||||
f"⚠️ 模型输出中断(收到部分内容后连接断开):{call_error}。本轮任务失败。",
|
||||
is_final=True,
|
||||
)
|
||||
raise call_error
|
||||
|
||||
if usage:
|
||||
self._apply_usage(usage)
|
||||
|
||||
@ -443,7 +536,7 @@ class SubAgentTask:
|
||||
try:
|
||||
from modules.multi_agent.state import build_sub_agent_output_text
|
||||
msg = build_sub_agent_output_text(self.display_name, output_text.strip(), is_final=is_final)
|
||||
# 如果该 agent 正被 sleep(wait_sub_agent_output_ids) 等待,把输出交给等待方,
|
||||
# 如果该 agent 正被 sleep(wait_sub_agent_output) 等待,把输出交给等待方,
|
||||
# 不再插入主对话(避免同一条消息出现两遍)
|
||||
if self.multi_agent_state.claim_output_wait(self.agent_id, msg):
|
||||
ma_debug(
|
||||
@ -651,53 +744,96 @@ class SubAgentTask:
|
||||
model_key: str,
|
||||
tools: List[Dict[str, Any]],
|
||||
) -> tuple:
|
||||
"""调用模型并解析 assistant 消息。"""
|
||||
"""调用模型并解析 assistant 消息。
|
||||
|
||||
失败时抛出 SubAgentModelCallError,携带 received_any 标记区分
|
||||
「请求阶段失败(可重试)」与「输出期间断开(直接失败)」。
|
||||
"""
|
||||
assistant_message = ""
|
||||
reasoning = ""
|
||||
tool_calls: List[Dict[str, Any]] = []
|
||||
usage = None
|
||||
# 是否已收到任何实质内容(文本/思考/工具调用)
|
||||
received_any = False
|
||||
# 已发出「正在调用」进度事件的 tool_call index(避免重复发)
|
||||
calling_emitted: Set[int] = set()
|
||||
|
||||
async for chunk in client.chat(self.messages, tools=tools, stream=True):
|
||||
if self._soft_stop:
|
||||
# 软停止:优雅中断当前模型调用,后续由 _run_loop 丢弃半成品并进入 idle
|
||||
break
|
||||
if self._cancelled:
|
||||
# 硬取消:立即抛出 CancelledError,确保不会返回半成品的 tool_calls 继续执行
|
||||
raise asyncio.CancelledError()
|
||||
if chunk.get("error"):
|
||||
raise RuntimeError(f"API 调用失败: {chunk.get('error')}")
|
||||
choice = (chunk.get("choices") or [{}])[0]
|
||||
delta = choice.get("delta") or {}
|
||||
if delta.get("content"):
|
||||
assistant_message += delta["content"]
|
||||
if delta.get("reasoning_content"):
|
||||
reasoning += delta["reasoning_content"]
|
||||
elif delta.get("reasoning_details"):
|
||||
rd = delta["reasoning_details"]
|
||||
if isinstance(rd, list):
|
||||
reasoning += "".join(str(d.get("text") or "") for d in rd)
|
||||
elif isinstance(rd, str):
|
||||
reasoning += rd
|
||||
elif isinstance(rd, dict):
|
||||
reasoning += str(rd.get("text") or "")
|
||||
try:
|
||||
async for chunk in client.chat(self.messages, tools=tools, stream=True):
|
||||
if self._soft_stop:
|
||||
# 软停止:优雅中断当前模型调用,后续由 _run_loop 丢弃半成品并进入 idle
|
||||
break
|
||||
if self._cancelled:
|
||||
# 硬取消:立即抛出 CancelledError,确保不会返回半成品的 tool_calls 继续执行
|
||||
raise asyncio.CancelledError()
|
||||
if chunk.get("error"):
|
||||
error_info = chunk.get("error")
|
||||
if isinstance(error_info, dict):
|
||||
error_text = (
|
||||
error_info.get("error_message")
|
||||
or error_info.get("error_text")
|
||||
or str(error_info)
|
||||
)
|
||||
else:
|
||||
error_text = str(error_info)
|
||||
raise SubAgentModelCallError(f"API 调用失败: {error_text}", received_any=received_any)
|
||||
choice = (chunk.get("choices") or [{}])[0]
|
||||
delta = choice.get("delta") or {}
|
||||
if delta.get("content"):
|
||||
assistant_message += delta["content"]
|
||||
received_any = True
|
||||
if delta.get("reasoning_content"):
|
||||
reasoning += delta["reasoning_content"]
|
||||
received_any = True
|
||||
elif delta.get("reasoning_details"):
|
||||
received_any = True
|
||||
rd = delta["reasoning_details"]
|
||||
if isinstance(rd, list):
|
||||
reasoning += "".join(str(d.get("text") or "") for d in rd)
|
||||
elif isinstance(rd, str):
|
||||
reasoning += rd
|
||||
elif isinstance(rd, dict):
|
||||
reasoning += str(rd.get("text") or "")
|
||||
|
||||
for tc in delta.get("tool_calls") or []:
|
||||
idx = tc.get("index")
|
||||
if idx is None:
|
||||
continue
|
||||
while len(tool_calls) <= idx:
|
||||
tool_calls.append({"id": "", "type": "function", "function": {"name": "", "arguments": ""}})
|
||||
existing = tool_calls[idx]
|
||||
if tc.get("id"):
|
||||
existing["id"] = tc["id"]
|
||||
fn = tc.get("function") or {}
|
||||
if fn.get("name"):
|
||||
existing["function"]["name"] += fn["name"]
|
||||
if fn.get("arguments"):
|
||||
existing["function"]["arguments"] += fn["arguments"]
|
||||
for tc in delta.get("tool_calls") or []:
|
||||
idx = tc.get("index")
|
||||
if idx is None:
|
||||
continue
|
||||
received_any = True
|
||||
while len(tool_calls) <= idx:
|
||||
tool_calls.append({"id": "", "type": "function", "function": {"name": "", "arguments": ""}})
|
||||
existing = tool_calls[idx]
|
||||
if tc.get("id"):
|
||||
existing["id"] = tc["id"]
|
||||
fn = tc.get("function") or {}
|
||||
if fn.get("name"):
|
||||
existing["function"]["name"] += fn["name"]
|
||||
if fn.get("arguments"):
|
||||
existing["function"]["arguments"] += fn["arguments"]
|
||||
# 工具名与 id 就位后立刻发出「正在调用」进度事件,让前端在参数
|
||||
# 流式生成期间就能显示该工具条目(与主智能体的「正在调用工具」对齐)。
|
||||
# 与后续 running/completed 事件共用同一 tool_call id,前端按 id 更新同一条目
|
||||
if (
|
||||
idx not in calling_emitted
|
||||
and existing["function"]["name"]
|
||||
and existing.get("id")
|
||||
):
|
||||
calling_emitted.add(idx)
|
||||
self.emit("progress", {
|
||||
"id": existing["id"],
|
||||
"tool": existing["function"]["name"],
|
||||
"status": "calling",
|
||||
"args": {},
|
||||
"ts": int(time.time() * 1000),
|
||||
})
|
||||
|
||||
if chunk.get("usage"):
|
||||
usage = chunk["usage"]
|
||||
if chunk.get("usage"):
|
||||
usage = chunk["usage"]
|
||||
except (asyncio.CancelledError, SubAgentModelCallError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
# chat 流本身抛出的漏网异常(如底层连接错误未被转为 error chunk 时)
|
||||
raise SubAgentModelCallError(f"API 调用异常: {exc}", received_any=received_any) from exc
|
||||
|
||||
return assistant_message, reasoning, tool_calls, usage
|
||||
|
||||
@ -757,7 +893,7 @@ class SubAgentTask:
|
||||
return {"success": True, "answer": answer, "question_id": question_id}
|
||||
|
||||
if name == "ask_other_agent":
|
||||
target_id = int(args.get("target_agent_id") or 0)
|
||||
target_name = str(args.get("target_display_name") or "").strip()
|
||||
question = str(args.get("question") or "").strip()
|
||||
question_id = str(args.get("question_id") or f"ask_other_{uuid.uuid4().hex[:10]}")
|
||||
ma_debug(
|
||||
@ -765,21 +901,25 @@ class SubAgentTask:
|
||||
task_id=self.task_id,
|
||||
agent_id=self.agent_id,
|
||||
display_name=self.display_name,
|
||||
target_agent_id=target_id,
|
||||
target_display_name=target_name,
|
||||
question_id=question_id,
|
||||
question=question[:500],
|
||||
)
|
||||
if not target_id or not question:
|
||||
if not target_name or not question:
|
||||
return {"success": False, "error": "参数缺失"}
|
||||
# 查找目标实例
|
||||
target_inst = state.get_instance(target_id)
|
||||
# 显示名寻址:子智能体只知道对方的角色内编号显示名,
|
||||
# 全局 agent_id 在内部解析,不暴露给模型
|
||||
target_inst = state.get_instance_by_display_name(target_name)
|
||||
if not target_inst:
|
||||
return {"success": False, "error": f"agent {target_id} 不存在"}
|
||||
available = "、".join(
|
||||
n for n in state.list_display_names() if n != self.display_name
|
||||
) or "(无)"
|
||||
return {"success": False, "error": f"未找到子智能体「{target_name}」。当前可用: {available}"}
|
||||
# 构造提问消息并插入到目标子对话;同时要求其在下一轮调用 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)
|
||||
self.manager.inject_message_to_sub_agent(target_inst.agent_id, msg)
|
||||
# 阻塞等待回答
|
||||
try:
|
||||
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
|
||||
@ -788,7 +928,7 @@ class SubAgentTask:
|
||||
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 全局唯一,足以定位等待方,无需提问方编号
|
||||
question_id = str(args.get("question_id") or "")
|
||||
answer = str(args.get("answer") or "").strip()
|
||||
if not question_id or not answer:
|
||||
|
||||
@ -144,7 +144,7 @@
|
||||
当子智能体正在工作时,你的行为必须遵循以下规则:
|
||||
|
||||
- **严格禁止反复查看状态**:不要在子智能体运行期间反复调用 `get_sub_agent_status` 或 `list_active_sub_agents` 来查看进度。子智能体的输出会自动以消息形式插入你的对话,你不需要主动轮询。反复查状态是浪费时间的行为。
|
||||
- **等待子智能体输出的正确方式**:如果你没有需要立即处理的事情,可以选择**立刻停止输出**(等待子智能体的汇报消息自动到达),也可以选择使用 `sleep` 工具的 `wait_sub_agent_output_ids` 主动等待指定子智能体的下一次输出。不要在子智能体运行期间反复调用 `sleep` 做无意义的短延迟。
|
||||
- **等待子智能体输出的正确方式**:如果你没有需要立即处理的事情,可以选择**立刻停止输出**(等待子智能体的汇报消息自动到达),也可以选择使用 `sleep` 工具的 `wait_sub_agent_output`(传子智能体显示名)主动等待指定子智能体的下一次输出。不要在子智能体运行期间反复调用 `sleep` 做无意义的短延迟。
|
||||
- **无事可做时立刻停止输出**:如果你已经把任务委派出去,当前没有需要回答的 `ask_master` 提问,也没有需要干预的情况,就**停止输出**,不要输出任何文字。子智能体的产出会以 user 消息自动插入你的对话,届时你再继续工作。
|
||||
- **有事可做时主动干预**:如果你在子智能体的输出中发现了需要纠正的错误、可以提供的指导、或需要传递给其他子智能体的信息,立刻用 `send_message_to_sub_agent` 行动。这是你运行期间最有价值的工作。
|
||||
|
||||
@ -286,8 +286,9 @@ id: ask_fse_001
|
||||
## 关于显示名
|
||||
|
||||
- 主智能体固定显示名:`Team Leader`
|
||||
- 子智能体显示名:`{角色名}_{agent_id}`,如 `UI Operator_1`、`Full-Stack Engineer_2`
|
||||
- 一个角色可以有多个实例(同 role_id 多 agent_id)
|
||||
- 子智能体显示名:`{角色名}_{角色内编号}`,如 `UI Operator_1`、`Full-Stack Engineer_2`
|
||||
- 编号按角色独立递增:同一角色的多个实例编号依次为 1、2、3……;不同角色各自从 1 开始(例如已存在 Researcher_1~3 时,第一个 UI Operator 仍然是 UI Operator_1)
|
||||
- 所有涉及目标子智能体的工具参数一律使用显示名寻址:`send_message_to_sub_agent` / `stop_sub_agent` / `terminate_sub_agent` 传 `display_name`,`get_sub_agent_status` 传 `display_names`,`sleep` 的 `wait_sub_agent_output` 传显示名
|
||||
|
||||
## 关于通信协议的三条硬性原则
|
||||
|
||||
|
||||
@ -66,9 +66,10 @@
|
||||
- 工具会阻塞等待 Team Leader 通过 `answer_sub_agent_question` 给出回答
|
||||
- 你的 question 会以 XML「提问」格式被插入主对话
|
||||
- 提问要具体、可回答,不要问开放式问题让 Team Leader 猜你的意思
|
||||
- **要问其他子智能体时**:调用 `ask_other_agent`,传入 target_agent_id 与 question
|
||||
- **要问其他子智能体时**:调用 `ask_other_agent`,传入 target_display_name(对方显示名,如 Researcher_2)与 question
|
||||
- 等待对方调用 `answer_other_agent` 回答
|
||||
- **要回答其他子智能体的提问时**:调用 `answer_other_agent`,传入 source_agent_id 与 question_id 和 answer
|
||||
- 不确定有哪些队友时,先调用 `list_active_sub_agents` 查看
|
||||
- **要回答其他子智能体的提问时**:调用 `answer_other_agent`,传入 question_id 和 answer
|
||||
- 你的回答直接作为对方 `ask_other_agent` 工具的结果返回(不会以 user 消息插入对话)
|
||||
- **查询当前活跃子智能体**:调用 `list_active_sub_agents`
|
||||
|
||||
|
||||
@ -192,6 +192,8 @@ function renderDefaultResult(result: any, args: any, name: string): string {
|
||||
'file_path',
|
||||
'agent_id',
|
||||
'target_agent_id',
|
||||
'display_name',
|
||||
'target_display_name',
|
||||
'question',
|
||||
'role_id',
|
||||
'url'
|
||||
@ -204,6 +206,8 @@ function renderDefaultResult(result: any, args: any, name: string): string {
|
||||
file_path: '路径',
|
||||
agent_id: '子智能体 ID',
|
||||
target_agent_id: '目标子智能体 ID',
|
||||
display_name: '子智能体',
|
||||
target_display_name: '目标子智能体',
|
||||
question: '问题',
|
||||
role_id: '角色 ID',
|
||||
url: 'URL'
|
||||
@ -827,9 +831,9 @@ function renderSleep(result: any, args: any): string {
|
||||
html += `<div><strong>原因:</strong>${escapeHtml(String(args.reason))}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_output') {
|
||||
const agentId = result?.agent_id ?? args?.wait_sub_agent_output_ids ?? '';
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>等待子智能体:</strong>#${escapeHtml(String(agentId))}</div>`;
|
||||
const targetName = result?.display_name ?? args?.wait_sub_agent_output ?? '';
|
||||
if (targetName !== '') {
|
||||
html += `<div><strong>等待子智能体:</strong>${escapeHtml(String(targetName))}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_ids') {
|
||||
const agentIds = Array.isArray(result?.agent_ids) ? result.agent_ids : [];
|
||||
@ -1454,6 +1458,8 @@ function renderEasterEgg(result: any, args: any): string {
|
||||
function renderCreateSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已创建', '✗ 创建失败');
|
||||
const agentId = result.agent_id ?? args.agent_id ?? '';
|
||||
// 多智能体模式:只展示角色内编号显示名,全局 agent_id/task_id 不暴露
|
||||
const displayName = result.display_name ?? '';
|
||||
const taskId = result.task_id ?? '';
|
||||
const deliverablesDir = result.deliverables_dir ?? '';
|
||||
const taskDescription = args.task ?? '';
|
||||
@ -1474,13 +1480,15 @@ function renderCreateSubAgent(result: any, args: any): string {
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
if (displayName) {
|
||||
html += `<div><strong>子智能体:</strong>${escapeHtml(String(displayName))}</div>`;
|
||||
} else if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (taskId !== '') {
|
||||
if (taskId !== '' && !displayName) {
|
||||
html += `<div><strong>任务 ID:</strong>${escapeHtml(String(taskId))}</div>`;
|
||||
}
|
||||
if (deliverablesDir !== '') {
|
||||
if (deliverablesDir !== '' && !displayName) {
|
||||
html += `<div><strong>交付目录:</strong>${escapeHtml(String(deliverablesDir))}</div>`;
|
||||
}
|
||||
if (taskDescription) {
|
||||
@ -1518,16 +1526,19 @@ function renderCreateSubAgent(result: any, args: any): string {
|
||||
|
||||
function renderTerminateSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已关闭', '✗ 关闭失败');
|
||||
const displayName = result.display_name ?? args.display_name ?? '';
|
||||
const agentId = result.agent_id ?? args.agent_id ?? '';
|
||||
const taskId = result.task_id ?? '';
|
||||
const message = result.message ?? result.system_message ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
if (displayName) {
|
||||
html += `<div><strong>子智能体:</strong>${escapeHtml(String(displayName))}</div>`;
|
||||
} else if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (taskId !== '') {
|
||||
if (taskId !== '' && !displayName) {
|
||||
html += `<div><strong>任务 ID:</strong>${escapeHtml(String(taskId))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
@ -1561,7 +1572,9 @@ function renderGetSubAgentStatus(result: any): string {
|
||||
const summary = item.summary ?? item.final_result?.message ?? item.final_result?.summary ?? '';
|
||||
|
||||
html += '<div class="sub-agent-status-item">';
|
||||
html += `<div class="sub-agent-status-header">子智能体 #${escapeHtml(String(agentId))}</div>`;
|
||||
const itemDisplayName = item.display_name || '';
|
||||
const itemHeader = itemDisplayName || `子智能体 #${agentId}`;
|
||||
html += `<div class="sub-agent-status-header">${escapeHtml(String(itemHeader))}</div>`;
|
||||
|
||||
if (!found) {
|
||||
html += '<div class="tool-result-meta">';
|
||||
@ -1580,7 +1593,7 @@ function renderGetSubAgentStatus(result: any): string {
|
||||
} else {
|
||||
html += `<div><strong>状态:</strong>${escapeHtml(status || '未知')}</div>`;
|
||||
}
|
||||
if (taskId !== '') {
|
||||
if (taskId !== '' && !itemDisplayName) {
|
||||
html += `<div><strong>任务 ID:</strong>${escapeHtml(String(taskId))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
@ -1601,11 +1614,14 @@ function renderGetSubAgentStatus(result: any): string {
|
||||
|
||||
function renderSendMessageToSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已发送', '✗ 发送失败');
|
||||
const displayName = args.display_name ?? result.display_name ?? '';
|
||||
const agentId = args.agent_id ?? result.agent_id ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
if (displayName) {
|
||||
html += `<div><strong>子智能体:</strong>${escapeHtml(String(displayName))}</div>`;
|
||||
} else if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
@ -1727,7 +1743,8 @@ function renderListActiveSubAgents(result: any): string {
|
||||
const lastOutput = agent.last_output || '';
|
||||
|
||||
html += '<div class="sub-agent-status-item">';
|
||||
html += `<div class="sub-agent-status-header">#${escapeHtml(String(agentId))} ${escapeHtml(displayName)} [${escapeHtml(status)}]</div>`;
|
||||
// 只展示角色内编号显示名,不暴露全局 agent_id
|
||||
html += `<div class="sub-agent-status-header">${escapeHtml(displayName)} [${escapeHtml(status)}]</div>`;
|
||||
html += '<div class="tool-result-meta">';
|
||||
if (summary) {
|
||||
html += `<div><strong>任务:</strong>${escapeHtml(String(summary))}</div>`;
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
:class="[item.kind === 'tool' ? 'feed-tool' : 'feed-text', { 'is-new': animatedKeys.has(item.key) }]"
|
||||
>
|
||||
<template v-if="item.kind === 'tool'">
|
||||
<span v-if="item.state === 'running'" class="qd-tool-spinner"></span>
|
||||
<span v-if="item.state === 'running' || item.state === 'calling'" class="qd-tool-spinner"></span>
|
||||
<span v-else class="qd-tool-done">{{ item.state === 'failed' ? '✕' : '✓' }}</span>
|
||||
<span class="tool-name">{{ item.toolName }}</span>
|
||||
<span class="tool-param" :title="item.text">{{ item.text }}</span>
|
||||
@ -257,6 +257,7 @@ const tokensTitle = computed(() => `上下文 ${currentTokens.value.toLocaleStri
|
||||
|
||||
function normalizeStatus(status?: string) {
|
||||
if (status === 'running' || status === 'in_progress') return 'running';
|
||||
if (status === 'calling') return 'calling';
|
||||
if (status === 'completed' || status === 'done' || status === 'success') return 'completed';
|
||||
if (status === 'failed' || status === 'error') return 'failed';
|
||||
return status || 'running';
|
||||
@ -324,11 +325,24 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
|
||||
typeof entry.content === 'string'
|
||||
) {
|
||||
flushToolGroup();
|
||||
rawItems.push({
|
||||
// 语义顺序修正:工具「正在调用」事件在流式期间先于文本 output 落盘,
|
||||
// 但 assistant 消息里文本永远在 tool_calls 之前——把 output 插入到
|
||||
// 尾部连续的非终态工具条目(同一条 assistant 消息发起的调用)之前
|
||||
const outputItem: OutputTimelineItem = {
|
||||
kind: 'output',
|
||||
key: `output-${entry.ts || index}`,
|
||||
content: entry.content
|
||||
});
|
||||
};
|
||||
let cut = rawItems.length;
|
||||
while (cut > 0) {
|
||||
const tail = rawItems[cut - 1];
|
||||
if (tail.kind === 'tool' && !isEntryTerminal(tail.entry?.status)) {
|
||||
cut -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rawItems.splice(cut, 0, outputItem);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -346,6 +360,19 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
|
||||
return;
|
||||
}
|
||||
|
||||
// 同一 tool_call id 的历史条目仍非终态时原地更新(「正在调用」事件可能与其他
|
||||
// 工具的调用事件交错到达),避免出现永远转圈的重复条目
|
||||
if (entry.id) {
|
||||
const prior = rawItems.find(
|
||||
(item) =>
|
||||
item.kind === 'tool' && item.entry.id === entry.id && !isEntryTerminal(item.entry.status)
|
||||
);
|
||||
if (prior && prior.kind === 'tool') {
|
||||
prior.entry = { ...prior.entry, ...entry };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
flushToolGroup();
|
||||
let key = baseKey;
|
||||
let suffix = 0;
|
||||
@ -367,7 +394,7 @@ const timelineItems = computed<(ToolTimelineItem | OutputTimelineItem)[]>(() =>
|
||||
kind: 'tool' as const,
|
||||
key: item.key,
|
||||
state,
|
||||
stateLabel: state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中',
|
||||
stateLabel: state === 'completed' ? '完成' : state === 'failed' ? '失败' : state === 'calling' ? '调用中' : '进行中',
|
||||
toolName: item.entry.tool || '工具',
|
||||
text: buildText(item.entry)
|
||||
};
|
||||
|
||||
@ -84,6 +84,7 @@ const close = () => {
|
||||
|
||||
const normalizeStatus = (status?: string) => {
|
||||
if (status === 'running' || status === 'in_progress') return 'running';
|
||||
if (status === 'calling') return 'calling';
|
||||
if (status === 'completed' || status === 'done' || status === 'success') return 'completed';
|
||||
if (status === 'failed' || status === 'error') return 'failed';
|
||||
return status || 'running';
|
||||
@ -184,12 +185,25 @@ const timelineItems = computed(() => {
|
||||
entries.forEach((entry: ActivityEntry, index: number) => {
|
||||
if (entry?.type === 'progress' && entry?.subtype === 'output' && typeof entry.content === 'string') {
|
||||
flushToolGroup();
|
||||
rawItems.push({
|
||||
kind: 'output',
|
||||
// 语义顺序修正:工具「正在调用」事件在流式期间先于文本 output 落盘,
|
||||
// 但 assistant 消息里文本永远在 tool_calls 之前——把 output 插入到
|
||||
// 尾部连续的非终态工具条目(同一条 assistant 消息发起的调用)之前
|
||||
const outputItem = {
|
||||
kind: 'output' as const,
|
||||
key: `output-${entry.ts || index}`,
|
||||
content: entry.content,
|
||||
isFinal: !!entry.is_final,
|
||||
});
|
||||
};
|
||||
let cut = rawItems.length;
|
||||
while (cut > 0) {
|
||||
const tail = rawItems[cut - 1];
|
||||
if (tail.kind === 'tool' && !isTerminalStatus(tail.entry?.status)) {
|
||||
cut -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rawItems.splice(cut, 0, outputItem);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -205,6 +219,21 @@ const timelineItems = computed(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 同一 tool_call id 的历史条目仍非终态时原地更新(「正在调用」事件可能与其他
|
||||
// 工具的调用事件交错到达),避免出现永远转圈的重复条目
|
||||
if (entry.id) {
|
||||
const prior = rawItems.find(
|
||||
(item) =>
|
||||
item.kind === 'tool' &&
|
||||
item.entry.id === entry.id &&
|
||||
!isTerminalStatus(item.entry.status)
|
||||
);
|
||||
if (prior && prior.kind === 'tool') {
|
||||
prior.entry = { ...prior.entry, ...entry };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
flushToolGroup();
|
||||
let key = baseKey;
|
||||
let suffix = 0;
|
||||
@ -224,7 +253,7 @@ const timelineItems = computed(() => {
|
||||
kind: 'tool' as const,
|
||||
key: item.key,
|
||||
state,
|
||||
stateLabel: state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中',
|
||||
stateLabel: state === 'completed' ? '完成' : state === 'failed' ? '失败' : state === 'calling' ? '调用中' : '进行中',
|
||||
text: buildText(item.entry)
|
||||
};
|
||||
});
|
||||
|
||||
@ -2519,8 +2519,9 @@ body[data-theme='dark'] {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* hover 与输入栏按钮一致(--hover-bg),与菜单底色拉开对比 */
|
||||
.dropdown-item:hover:not(.disabled) {
|
||||
background: var(--surface-muted) !important;
|
||||
background: var(--hover-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -141,9 +141,13 @@ def _format_sub_agent_stats(stats: Optional[Dict[str, Any]]) -> str:
|
||||
def _format_create_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("create_sub_agent", result_data)
|
||||
# 多智能体模式:对模型只暴露角色内编号显示名,全局 agent_id/task_id 为内部细节
|
||||
display_name = result_data.get("display_name")
|
||||
status = result_data.get("status")
|
||||
if display_name:
|
||||
return f"{display_name} 已创建(状态 {status or 'running'})。"
|
||||
agent_id = result_data.get("agent_id")
|
||||
task_id = result_data.get("task_id")
|
||||
status = result_data.get("status")
|
||||
refs = result_data.get("copied_references") or []
|
||||
ref_note = f",附带 {len(refs)} 份参考文件" if refs else ""
|
||||
deliver_dir = result_data.get("deliverables_dir")
|
||||
@ -203,9 +207,11 @@ def _format_get_sub_agent_status(result_data: Dict[str, Any]) -> str:
|
||||
return "未找到子智能体状态。"
|
||||
blocks = []
|
||||
for item in results:
|
||||
# 多智能体模式:优先使用角色内编号显示名,不暴露全局 agent_id
|
||||
agent_id = item.get("agent_id")
|
||||
label = item.get("display_name") or f"#{agent_id}"
|
||||
if not item.get("found"):
|
||||
blocks.append(f"子智能体 #{agent_id} 不存在。")
|
||||
blocks.append(f"子智能体 {label} 不存在。")
|
||||
continue
|
||||
status = item.get("status")
|
||||
summary = None
|
||||
@ -221,13 +227,13 @@ def _format_get_sub_agent_status(result_data: Dict[str, Any]) -> str:
|
||||
stats_text = _format_sub_agent_stats(item.get("stats"))
|
||||
|
||||
if status == "completed":
|
||||
lines = [f"子智能体 #{agent_id} 已完成"]
|
||||
lines = [f"子智能体 {label} 已完成"]
|
||||
elif status == "terminated":
|
||||
lines = [f"子智能体 #{agent_id} 已终止"]
|
||||
lines = [f"子智能体 {label} 已终止"]
|
||||
elif status in {"failed", "timeout"}:
|
||||
lines = [f"⚠️ 子智能体 #{agent_id} 状态 {status}"]
|
||||
lines = [f"⚠️ 子智能体 {label} 状态 {status}"]
|
||||
else:
|
||||
lines = [f"子智能体 #{agent_id} 状态: {status}"]
|
||||
lines = [f"子智能体 {label} 状态: {status}"]
|
||||
if stats_text:
|
||||
lines.append(stats_text)
|
||||
if status == "completed" and isinstance(elapsed_seconds, (int, float)):
|
||||
@ -250,6 +256,9 @@ def _format_close_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
def _format_terminate_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("terminate_sub_agent", result_data)
|
||||
display_name = result_data.get("display_name")
|
||||
if display_name:
|
||||
return f"已强制关闭子智能体 {display_name}。"
|
||||
agent_id = result_data.get("agent_id")
|
||||
task_id = result_data.get("task_id")
|
||||
message = result_data.get("message") or "子智能体已被强制关闭。"
|
||||
@ -261,6 +270,9 @@ def _format_terminate_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
def _format_send_message_to_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("send_message_to_sub_agent", result_data)
|
||||
display_name = result_data.get("display_name")
|
||||
if display_name:
|
||||
return f"已向 {display_name} 发送消息。"
|
||||
agent_id = result_data.get("agent_id")
|
||||
if agent_id is not None:
|
||||
return f"已向子智能体 #{agent_id} 发送消息。"
|
||||
@ -270,8 +282,12 @@ def _format_send_message_to_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
def _format_stop_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("stop_sub_agent", result_data)
|
||||
agent_id = result_data.get("agent_id")
|
||||
display_name = result_data.get("display_name")
|
||||
message = result_data.get("message") or "子智能体已暂停。"
|
||||
if display_name:
|
||||
# manager 返回的 message 已是「{显示名} 已暂停…」格式,直接返回避免重复
|
||||
return message
|
||||
agent_id = result_data.get("agent_id")
|
||||
if agent_id is not None:
|
||||
return f"已暂停子智能体 #{agent_id}。{message}"
|
||||
return message
|
||||
@ -332,7 +348,8 @@ def _format_active_sub_agents_list(agents: List[Dict[str, Any]]) -> str:
|
||||
status = agent.get("status") or "unknown"
|
||||
summary = agent.get("summary") or ""
|
||||
last_output = agent.get("last_output") or ""
|
||||
lines.append(f"#{agent_id} {display_name} [{status}]")
|
||||
# 只暴露角色内编号显示名,不暴露全局 agent_id
|
||||
lines.append(f"{display_name} [{status}]")
|
||||
if summary:
|
||||
lines.append(f" 任务:{summary}")
|
||||
if last_output:
|
||||
|
||||
@ -144,8 +144,7 @@ def _format_sleep(result_data: Dict[str, Any]) -> str:
|
||||
mode = result_data.get("mode")
|
||||
if mode == "wait_sub_agent_output":
|
||||
message = result_data.get("message") or ""
|
||||
agent_id = result_data.get("agent_id")
|
||||
header = f"已收到子智能体 {agent_id} 的输出"
|
||||
header = f"已收到 {result_data.get('display_name') or '子智能体'} 的输出"
|
||||
if message:
|
||||
return f"{header}\n\n{message}"
|
||||
return header
|
||||
|
||||
Loading…
Reference in New Issue
Block a user