diff --git a/AGENTS.md b/AGENTS.md index 8d07387e..4c0b5215 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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()` 自动分配(对话级最小空闲正整数),两者是两套独立命名空间,不得混用。 --- diff --git a/core/main_terminal_parts/tools_definition/core_tools.py b/core/main_terminal_parts/tools_definition/core_tools.py index 9b141101..e45b938d 100644 --- a/core/main_terminal_parts/tools_definition/core_tools.py +++ b/core/main_terminal_parts/tools_definition/core_tools.py @@ -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", diff --git a/core/main_terminal_parts/tools_definition/terminal_tools.py b/core/main_terminal_parts/tools_definition/terminal_tools.py index 4fdce0b5..c37f5e82 100644 --- a/core/main_terminal_parts/tools_definition/terminal_tools.py +++ b/core/main_terminal_parts/tools_definition/terminal_tools.py @@ -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"] diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index 9e424b22..6339fee0 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -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)} diff --git a/modules/multi_agent/state.py b/modules/multi_agent/state.py index b40cdb52..a154fa6d 100644 --- a/modules/multi_agent/state.py +++ b/modules/multi_agent/state.py @@ -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 diff --git a/modules/multi_agent/tools.py b/modules/multi_agent/tools.py index 6b020296..14bb129d 100644 --- a/modules/multi_agent/tools.py +++ b/modules/multi_agent/tools.py @@ -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"], }, }, } diff --git a/modules/sub_agent/creation.py b/modules/sub_agent/creation.py index 46a16148..da78537c 100644 --- a/modules/sub_agent/creation.py +++ b/modules/sub_agent/creation.py @@ -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: diff --git a/modules/sub_agent/manager.py b/modules/sub_agent/manager.py index 4baa0f63..4b629916 100644 --- a/modules/sub_agent/manager.py +++ b/modules/sub_agent/manager.py @@ -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"), diff --git a/modules/sub_agent/task.py b/modules/sub_agent/task.py index d6a6bf85..528fe8c7 100644 --- a/modules/sub_agent/task.py +++ b/modules/sub_agent/task.py @@ -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: diff --git a/prompts/multi_agent/master.txt b/prompts/multi_agent/master.txt index 34c7dfab..f43a245d 100644 --- a/prompts/multi_agent/master.txt +++ b/prompts/multi_agent/master.txt @@ -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` 传显示名 ## 关于通信协议的三条硬性原则 diff --git a/prompts/multi_agent/sub_agent.txt b/prompts/multi_agent/sub_agent.txt index 1a1c3558..8cbb0c59 100644 --- a/prompts/multi_agent/sub_agent.txt +++ b/prompts/multi_agent/sub_agent.txt @@ -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` diff --git a/static/src/components/chat/actions/toolRenderers.ts b/static/src/components/chat/actions/toolRenderers.ts index fe6be8ed..dcb3449d 100644 --- a/static/src/components/chat/actions/toolRenderers.ts +++ b/static/src/components/chat/actions/toolRenderers.ts @@ -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 += `