From 67fdb715f5277e3ebc7482282203261c7447a886 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sat, 23 May 2026 01:58:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(messages):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=9C=9F=E6=B6=88=E6=81=AF=E6=8F=92=E5=85=A5?= =?UTF-8?q?=E4=B8=BA=20[=E7=B3=BB=E7=BB=9F=E9=80=9A=E7=9F=A5|source]=20?= =?UTF-8?q?=E5=89=8D=E7=BC=80=E7=9A=84=20user=20=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 server/chat_flow_task_support.inject_runtime_user_message 作为唯一入口, 统一处理 add_conversation + messages.insert + sender(user_message) 三件事 - 子智能体 / 后台 run_command / runtime_guidance / mode notice 全部走该入口, role 一律 user,inline 仅作 metadata 标记,不再复用 system role 表达语义 - inline poll 移出 tool 执行循环,改为整轮 tool 完成后批量注入, 避免在 assistant.tool_calls 与 tool 序列之间插入 user 导致 API 报错 - 删除 _insert_completion_notice_message / _record_sub_agent_message 以及 build_messages 中 system→user 的回转分支 - 移除已废弃的 wait_sub_agent 工具残留(后端注册、前端图标/动画、 prompts、SKILL 文档;保留 sleep 的 wait_sub_agent_ids 参数) - 前端取消对 is_auto_generated/auto_message_type 的过滤, 让运行期注入的 user 消息在历史和实时推送中正常渲染 Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 1 - agentskills/run-command-guide/SKILL.md | 2 +- agentskills/sub-agent-guide/SKILL.md | 6 +- core/main_terminal_parts/commands.py | 12 +- core/main_terminal_parts/context.py | 6 - core/main_terminal_parts/tools_execution.py | 21 --- core/tool_config.py | 2 +- modules/sub_agent_manager.py | 2 +- prompts/main_system.txt | 2 +- prompts/main_system_qwenvl.txt | 2 +- server/chat.py | 45 ++--- server/chat_flow_task_support.py | 186 ++++++++++++-------- server/chat_flow_tool_loop.py | 146 ++++++--------- static/src/app/methods/history.ts | 8 - static/src/app/methods/taskPolling.ts | 32 ++-- static/src/components/chat/ChatArea.vue | 3 - static/src/utils/chatDisplay.ts | 3 +- static/src/utils/icons.ts | 1 - utils/tool_result_formatter.py | 1 - 19 files changed, 224 insertions(+), 257 deletions(-) diff --git a/README.md b/README.md index 495afff..92babe2 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,6 @@ python3 -m server.app --path <当前目录> --port 8091 --thinking-mode 工具接口: - `create_sub_agent`: 创建并启动子智能体 -- `wait_sub_agent`: 等待子智能体完成并获取结果 - `close_sub_agent`: 终止运行中的子智能体 ### 🛠️ 丰富的工具集 diff --git a/agentskills/run-command-guide/SKILL.md b/agentskills/run-command-guide/SKILL.md index 2591379..5e01617 100644 --- a/agentskills/run-command-guide/SKILL.md +++ b/agentskills/run-command-guide/SKILL.md @@ -38,7 +38,7 @@ description: run_command 工具使用指南。介绍前台与后台模式、后 - timeout 最长 3600 秒。命令在后台继续执行,工具会等约 5 秒收集当前输出并返回,然后 **立即释放控制权**,让你继续处理其他任务。 - 返回值里会包含 `command_id`,`status` 可能是 `running_background`。你会拿到已经产生的输出片段,但最终结果要靠系统通知或手动等待。 -- 成功或失败完成后,系统会自动发送一条 `system` 消息(内容类似 `后台命令已完成(completed)`,并附带 `command_id`、`command`、`output` 摘要)通知你。不要再依赖 `wait_sub_agent`/手动轮询。 +- 成功或失败完成后,系统会自动插入一条 user 消息(前缀 `[系统通知|background_command]`,附带 `command_id` 与输出摘要)通知你。不要依赖任何手动轮询。 - 适合长时间任务(构建/安装/批处理)或你想“先发起再继续别的工作”。 - 返回的 `command_id` 需要保留,用于后续通过 `sleep(wait_runcommand_id=...)` 等待结果或根据通知定位输出。 diff --git a/agentskills/sub-agent-guide/SKILL.md b/agentskills/sub-agent-guide/SKILL.md index acc97d5..c189750 100644 --- a/agentskills/sub-agent-guide/SKILL.md +++ b/agentskills/sub-agent-guide/SKILL.md @@ -122,7 +122,7 @@ create_sub_agent( - 你会在对话中看到类似:"这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成..." **完成通知机制说明:** -- 后台子智能体完成的通知是系统自动插入的 user/system 消息,不依赖手动调用 `wait_sub_agent` 或其它轮询接口。 +- 后台子智能体完成的通知是系统自动插入的 user 消息(前缀 `[系统通知|sub_agent]`),不依赖手动调用任何轮询接口。 - 你只需在收到通知后直接查看交付目录即可,系统会把所有必需信息一并带上。 **何时使用:** @@ -163,8 +163,8 @@ create_sub_agent(agent_id=1, task="大规模日志分析", run_in_background=tru **重要提示:** - 系统会自动处理通知,你只需要在收到通知消息后处理结果即可 - 如果你正在执行工具调用,系统会延迟通知,避免打断你的工作流 -- 通知类型取决于你是否继续工作:继续工作时收到 system 消息,停止输出时收到 user 消息 -- 后台完成由上述系统通知负责,不要再依赖 `wait_sub_agent` 或类似的手动等待流,系统会在能插入消息时自动通知你。 +- 不论运行中还是空闲,通知一律是带 `[系统通知|sub_agent]` 前缀的 user 消息;运行中只是通知不触发新一轮回复,空闲时等同于一条新输入会触发回复 +- 后台完成由上述系统通知负责,不要再依赖任何手动等待流,系统会在能插入消息时自动通知你。 ### 模式选择决策树 diff --git a/core/main_terminal_parts/commands.py b/core/main_terminal_parts/commands.py index d85b84a..c009d1e 100644 --- a/core/main_terminal_parts/commands.py +++ b/core/main_terminal_parts/commands.py @@ -332,7 +332,17 @@ class MainTerminalCommandMixin: ) system_message = tool_result.get("system_message") if system_message: - self._record_sub_agent_message(system_message, tool_result.get("task_id"), inline=False) + from server.chat_flow_task_support import inject_runtime_user_message + inject_runtime_user_message( + web_terminal=self, + messages=None, + text=system_message, + source="sub_agent", + sender=None, + conversation_id=getattr(self.context_manager, "current_conversation_id", None), + inline=False, + extra_metadata={"task_id": tool_result.get("task_id")}, + ) # 4. 在终端显示执行信息(不保存到历史) if collected_tool_calls: diff --git a/core/main_terminal_parts/context.py b/core/main_terminal_parts/context.py index 1465370..c57e1c0 100644 --- a/core/main_terminal_parts/context.py +++ b/core/main_terminal_parts/context.py @@ -431,12 +431,6 @@ class MainTerminalContextMixin: } messages.append(message) - elif conv["role"] == "system" and metadata.get("sub_agent_notice"): - # 转换为用户消息,让模型能及时响应 - messages.append({ - "role": "user", - "content": conv["content"] - }) else: # User 或普通 System 消息 images = conv.get("images") or metadata.get("images") or [] diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index 408f47f..2b2b03a 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -164,7 +164,6 @@ class MainTerminalToolsExecutionMixin: } _SUB_AGENT_SERIES_TOOLS = { "create_sub_agent", - "wait_sub_agent", "close_sub_agent", "terminate_sub_agent", "get_sub_agent_status", @@ -198,26 +197,6 @@ class MainTerminalToolsExecutionMixin: "terminal_session", } - def _record_sub_agent_message(self, message: Optional[str], task_id: Optional[str] = None, inline: bool = False): - """以 system 消息记录子智能体状态。""" - if not message: - return - if task_id and task_id in self._announced_sub_agent_tasks: - return - if task_id: - self._announced_sub_agent_tasks.add(task_id) - logger.info( - "[SubAgent] record message | task=%s | inline=%s | content=%s", - task_id, - inline, - message.replace("\n", "\\n")[:200], - ) - metadata = {"sub_agent_notice": True, "inline": inline, "is_auto_generated": True, "auto_message_type": "sub_agent_notice"} - if task_id: - metadata["task_id"] = task_id - self.context_manager.add_conversation("system", message, metadata=metadata) - print(f"{OUTPUT_FORMATS['info']} {message}") - # 扩展的只读命令白名单(包含常用管道命令) _READONLY_ALLOWED_EXECUTABLES = { "grep", "find", "ls", "pwd", "tree", "cat", "head", "tail", diff --git a/core/tool_config.py b/core/tool_config.py index d52d29b..27313f2 100644 --- a/core/tool_config.py +++ b/core/tool_config.py @@ -75,7 +75,7 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = { ), "sub_agent": ToolCategory( label="子智能体", - tools=["create_sub_agent", "wait_sub_agent", "close_sub_agent"], + tools=["create_sub_agent", "close_sub_agent"], ), "easter_egg": ToolCategory( label="彩蛋实验", diff --git a/modules/sub_agent_manager.py b/modules/sub_agent_manager.py index f392ec6..78d61be 100644 --- a/modules/sub_agent_manager.py +++ b/modules/sub_agent_manager.py @@ -958,7 +958,7 @@ class SubAgentManager: return None def lookup_task(self, *, task_id: Optional[str] = None, agent_id: Optional[int] = None) -> Optional[Dict]: - """只读查询任务信息,供 wait_sub_agent 自动调整超时时间。""" + """只读查询任务信息,供 sleep(wait_sub_agent_ids=...) 自动调整超时时间。""" task = self._select_task(task_id, agent_id) if not task: return None diff --git a/prompts/main_system.txt b/prompts/main_system.txt index a4788b8..b0a52e5 100644 --- a/prompts/main_system.txt +++ b/prompts/main_system.txt @@ -138,7 +138,7 @@ ## 5. 操作技巧 - **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新 -- **子智能体**:独立子任务并行处理,最多5个同时运行,`wait_sub_agent` 超时需≥创建时设置;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解 +- **子智能体**:独立子任务并行处理,最多5个同时运行;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解 - **记忆管理**:`update_memory` 支持追加/替换/删除,main 为长期记忆,task 为当前对话记忆 --- diff --git a/prompts/main_system_qwenvl.txt b/prompts/main_system_qwenvl.txt index d5dcc67..ae7abd3 100644 --- a/prompts/main_system_qwenvl.txt +++ b/prompts/main_system_qwenvl.txt @@ -155,7 +155,7 @@ ## 5. 操作技巧 - **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新 -- **子智能体**:独立子任务并行处理,最多5个同时运行,`wait_sub_agent` 超时需≥创建时设置;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解 +- **子智能体**:独立子任务并行处理,最多5个同时运行;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解 - **记忆管理**:`update_memory` 支持追加/替换/删除,main 为长期记忆,task 为当前对话记忆 --- diff --git a/server/chat.py b/server/chat.py index 83abcf6..eeef886 100644 --- a/server/chat.py +++ b/server/chat.py @@ -53,12 +53,6 @@ def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: st notice = str(text or "").strip() if not notice: return - metadata = { - "runtime_guidance": True, - "runtime_guidance_original": notice, - "runtime_mode_notice": True, - "message_source": "notify", - } # 若有运行任务,优先入队到该任务(下一轮工具后注入),确保会进入后续 API 请求上下文。 try: @@ -84,26 +78,25 @@ def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: st pass # 无运行任务时,直接写入当前会话并广播前端显示(来源固定为 notify)。 - try: - if getattr(terminal, "context_manager", None): - terminal.context_manager.add_conversation("user", notice, metadata=metadata) - except Exception: - pass - try: - socketio.emit( - "user_message", - { - "message": notice, - "runtime_guidance": True, - "runtime_guidance_original": notice, - "runtime_mode_notice": True, - "message_source": "notify", - "conversation_id": getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None), - }, - room=f"user_{username}", - ) - except Exception: - pass + from .chat_flow_task_support import inject_runtime_user_message + + conversation_id = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None) + + def _emit(event: str, payload: Dict[str, Any]) -> None: + try: + socketio.emit(event, payload, room=f"user_{username}") + except Exception: + pass + + inject_runtime_user_message( + web_terminal=terminal, + messages=None, + text=notice, + source="notify", + sender=_emit, + conversation_id=conversation_id, + inline=True, + ) @chat_bp.route('/api/thinking-mode', methods=['POST']) @api_login_required diff --git a/server/chat_flow_task_support.py b/server/chat_flow_task_support.py index 9f38116..382eb76 100644 --- a/server/chat_flow_task_support.py +++ b/server/chat_flow_task_support.py @@ -2,43 +2,110 @@ from __future__ import annotations import asyncio import time -from typing import Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional from modules.sub_agent_manager import TERMINAL_STATUSES -def _insert_completion_notice_message( +_VALID_SOURCES = { + "guidance", + "notify", + "user", + "presend", + "sub_agent", + "background_command", +} + + +def inject_runtime_user_message( *, - messages: List[Dict], - after_tool_call_id: Optional[str], - message: str, + web_terminal, + messages: Optional[List[Dict]], + text: str, + source: str, + sender: Optional[Callable[[str, Dict[str, Any]], None]] = None, + conversation_id: Optional[str] = None, + after_tool_call_id: Optional[str] = None, inline: bool, - sender, - metadata: Optional[Dict] = None, - sub_agent_notice: bool = False, -): - """统一插入完成通知(system/user)并发送事件。""" - insert_index = len(messages) - if after_tool_call_id: - for idx, msg in enumerate(messages): - if msg.get("role") == "tool" and msg.get("tool_call_id") == after_tool_call_id: - insert_index = idx + 1 - break + extra_metadata: Optional[Dict[str, Any]] = None, + persist: bool = True, +) -> Optional[str]: + """运行期/空闲期统一向对话插入一条 user 消息(带 [系统通知|{source}] 前缀)。 - insert_role = "system" if inline else "user" - meta = dict(metadata or {}) - meta["inline"] = inline - messages.insert(insert_index, { - "role": insert_role, - "content": message, - "metadata": meta - }) + - inline=True 表示运行期插入(仅作为通知,不主动触发新一轮回复)。 + - inline=False 表示空闲期插入(等价于一条用户消息,由调用方决定是否启动新一轮回复)。 + - messages=None 时仅持久化到对话历史并广播前端(无运行中的 messages 列表可写)。 + """ + raw = "" if text is None else str(text).strip() + if not raw: + return None - payload = {'content': message, 'inline': inline} - if sub_agent_notice: - payload['sub_agent_notice'] = True - payload.update({k: v for k, v in meta.items() if k in {"command_id", "task_id", "background_command_notice"}}) - sender('system_message', payload) + src = str(source or "").strip().lower() + if src not in _VALID_SOURCES: + src = "guidance" + + formatted = f"[系统通知|{src}]\n{raw}" + + metadata: Dict[str, Any] = { + "runtime_injected": True, + "source": src, + "inline": inline, + "is_auto_generated": True, + "auto_message_type": f"runtime_{src}", + "message_source": src, + "runtime_guidance": True, + "runtime_guidance_original": raw, + } + if extra_metadata: + metadata.update({k: v for k, v in extra_metadata.items() if v is not None}) + + if persist: + try: + ctx_manager = getattr(web_terminal, "context_manager", None) + if ctx_manager is not None: + ctx_manager.add_conversation("user", formatted, metadata=metadata) + except Exception: + pass + + if messages is not None: + insert_index = len(messages) + if after_tool_call_id: + for idx, msg in enumerate(messages): + if msg.get("role") == "tool" and msg.get("tool_call_id") == after_tool_call_id: + # 跳过紧随其后的所有 tool 消息,避免把 user 插进同一 assistant.tool_calls 的 tool 序列中间, + # 否则会触发 "insufficient tool messages following tool_calls" 错误。 + end = idx + 1 + while end < len(messages) and messages[end].get("role") == "tool": + end += 1 + insert_index = end + break + messages.insert(insert_index, { + "role": "user", + "content": formatted, + "metadata": dict(metadata), + }) + + if callable(sender): + payload: Dict[str, Any] = { + "message": formatted, + "content": formatted, + "conversation_id": conversation_id, + "inline": inline, + "source": src, + "message_source": src, + "runtime_guidance": True, + "runtime_guidance_original": raw, + } + for key in ("task_id", "command_id"): + value = (extra_metadata or {}).get(key) + if value is not None: + payload[key] = value + try: + sender("user_message", payload) + except Exception: + pass + + return formatted async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message): @@ -126,13 +193,6 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals debug_log(f"[SubAgent] update task={task_id} inline={inline} msg={message}") - # 记录到对话历史(用于后续 build_messages 转换为 user 消息) - if hasattr(web_terminal, "_record_sub_agent_message"): - try: - web_terminal._record_sub_agent_message(message, task_id, inline=inline) - except Exception as exc: - debug_log(f"[SubAgent] 记录子智能体消息失败: {exc}") - # 标记任务已通知 if task_id: web_terminal._announced_sub_agent_tasks.add(task_id) @@ -144,19 +204,17 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals except Exception as exc: debug_log(f"[SubAgent] 保存通知状态失败: {exc}") - # 运行中插入 system 消息,避免触发新的 user 轮次;非运行中保持 user 通知 - if not inline: - prefix = "这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成" - if not message.startswith(prefix): - message = f"{prefix}\n\n{message}" - _insert_completion_notice_message( + conversation_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None) + inject_runtime_user_message( + web_terminal=web_terminal, messages=messages, - after_tool_call_id=after_tool_call_id, - message=message, - inline=inline, + text=message, + source="sub_agent", sender=sender, - metadata={"sub_agent_notice": True, "task_id": task_id}, - sub_agent_notice=True, + conversation_id=conversation_id, + after_tool_call_id=after_tool_call_id, + inline=inline, + extra_metadata={"task_id": task_id}, ) if inline: web_terminal._inline_sub_agent_notified.add(inline_key) @@ -194,34 +252,16 @@ async def process_background_command_updates(*, messages: List[Dict], inline: bo f"output_len={len(output)} inline={inline}" ) - # 与子智能体 inline 通知一致:写入对话历史,确保前端/历史都可见 - try: - web_terminal.context_manager.add_conversation( - "system", - message, - metadata={ - "background_command_notice": True, - "inline": inline, - "command_id": command_id, - "is_auto_generated": True, - "auto_message_type": "background_command_notice", - }, - ) - debug_log(f"[BgCmdDebug] persisted system notice command_id={command_id}") - except Exception as exc: - debug_log(f"[BgCmdDebug] persist system notice failed command_id={command_id}: {exc}") - - _insert_completion_notice_message( + inject_runtime_user_message( + web_terminal=web_terminal, messages=messages, - after_tool_call_id=after_tool_call_id, - message=message, - inline=inline, + text=message, + source="background_command", sender=sender, - metadata={ - "background_command_notice": True, - "command_id": command_id, - }, - sub_agent_notice=False, + conversation_id=conversation_id, + after_tool_call_id=after_tool_call_id, + inline=inline, + extra_metadata={"command_id": command_id}, ) try: manager.mark_notified(str(command_id)) diff --git a/server/chat_flow_tool_loop.py b/server/chat_flow_tool_loop.py index 5319be1..a82777f 100644 --- a/server/chat_flow_tool_loop.py +++ b/server/chat_flow_tool_loop.py @@ -22,6 +22,7 @@ from config import TOOL_CALL_COOLDOWN from modules.personalization_manager import load_personalization_config, resolve_context_compression_settings from modules.auto_approval_service import run_auto_approval from .deep_compression import run_deep_compression +from .chat_flow_task_support import inject_runtime_user_message def _format_numbered_lines(lines: List[str], start_line_no: int) -> List[Dict[str, Any]]: @@ -186,44 +187,15 @@ def _inject_runtime_mode_notice( sender=None, conversation_id: Optional[str] = None, ) -> None: - if not isinstance(content, str) or not content.strip(): - return - text = content.strip() - metadata = { - "runtime_guidance": True, - "runtime_guidance_original": text, - "runtime_mode_notice": True, - "message_source": "notify", - } - try: - web_terminal.context_manager.add_conversation( - "user", - text, - metadata=metadata, - ) - except Exception: - pass - messages.append( - { - "role": "user", - "content": text, - "metadata": metadata, - } + inject_runtime_user_message( + web_terminal=web_terminal, + messages=messages, + text=content, + source="notify", + sender=sender, + conversation_id=conversation_id, + inline=True, ) - if callable(sender): - try: - sender( - "user_message", - { - "message": text, - "conversation_id": conversation_id, - "runtime_guidance": True, - "runtime_guidance_original": text, - "message_source": "notify", - }, - ) - except Exception: - pass def _format_rejected_tool_text(reason: str) -> str: @@ -863,18 +835,6 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie action_message = summary debug_log(f"{function_name} 执行完成: {summary or '无摘要'}") - if function_name == "wait_sub_agent": - system_msg = result_data.get("system_message") - if system_msg: - messages.append({ - "role": "system", - "content": system_msg - }) - sender('system_message', { - 'content': system_msg, - 'inline': False - }) - maybe_mark_failure_from_message(web_terminal, system_msg) if function_name == "sleep": try: if isinstance(result_data, dict) and result_data.get("mode") == "wait_sub_agent_ids": @@ -1121,11 +1081,20 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie except Exception as exc: debug_log(f"[ContextCompression] 同步替换本轮 messages 失败: {exc}") debug_log(f"💾 增量保存:工具结果 {function_name}") - if function_name != "wait_sub_agent": - system_message = result_data.get("system_message") if isinstance(result_data, dict) else None - if system_message: - web_terminal._record_sub_agent_message(system_message, result_data.get("task_id"), inline=False) - maybe_mark_failure_from_message(web_terminal, system_message) + system_message = result_data.get("system_message") if isinstance(result_data, dict) else None + if system_message: + inject_runtime_user_message( + web_terminal=web_terminal, + messages=messages, + text=system_message, + source="sub_agent", + sender=sender, + conversation_id=conversation_id, + after_tool_call_id=tool_call_id, + inline=False, + extra_metadata={"task_id": result_data.get("task_id")}, + ) + maybe_mark_failure_from_message(web_terminal, system_message) # 自动深层压缩(工具调用后触发) current_context_tokens = web_terminal.context_manager.get_current_context_tokens(conversation_id) @@ -1176,18 +1145,36 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie except Exception as exc: debug_log(f"[RuntimeMode] 应用挂起模式变更失败: {exc}") - debug_log(f"[SubAgent] after tool={function_name} call_id={tool_call_id} -> poll updates") - await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message) - debug_log(f"[BgCmdDebug] after tool={function_name} call_id={tool_call_id} -> poll background command updates (inline)") - await process_background_command_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message) - await asyncio.sleep(0.2) if tool_failed: mark_force_thinking(web_terminal, reason=f"{function_name}_failed") + # 同一轮全部 tool_call 完成后,再统一 poll 子智能体/后台命令并注入通知, + # 避免在 assistant.tool_calls 与对应 tool 消息序列之间插入 user 消息导致 API 报错。 + debug_log("[SubAgent] poll updates after all tool_calls finished") + await process_sub_agent_updates( + messages=messages, + inline=True, + after_tool_call_id=None, + web_terminal=web_terminal, + sender=sender, + debug_log=debug_log, + maybe_mark_failure_from_message=maybe_mark_failure_from_message, + ) + debug_log("[BgCmdDebug] poll background command updates after all tool_calls finished") + await process_background_command_updates( + messages=messages, + inline=True, + after_tool_call_id=None, + web_terminal=web_terminal, + sender=sender, + debug_log=debug_log, + maybe_mark_failure_from_message=maybe_mark_failure_from_message, + ) + # 运行期模式通知:必须等待同一轮全部 tool_call 都完成后再注入, - # 避免在 assistant.tool_calls 与对应 tool 消息之间插入 system 消息导致 API 报错。 + # 避免在 assistant.tool_calls 与对应 tool 消息之间插入 user 消息导致 API 报错。 if pending_runtime_mode_notices: for notice in pending_runtime_mode_notices: _inject_runtime_mode_notice( @@ -1222,39 +1209,16 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie ) else: runtime_guidance_text = str(raw_item or "").strip() - if runtime_guidance_source not in {"guidance", "notify", "user", "presend", "sub_agent", "background_command"}: - runtime_guidance_source = "guidance" if not runtime_guidance_text: continue - runtime_guidance_meta = { - "runtime_guidance": True, - "runtime_guidance_original": runtime_guidance_text, - "message_source": runtime_guidance_source, - } - try: - web_terminal.context_manager.add_conversation( - "user", - runtime_guidance_text, - metadata=runtime_guidance_meta, - ) - except Exception as exc: - debug_log(f"[RuntimeGuidance] 持久化引导消息失败: {exc}") - messages.append( - { - "role": "user", - "content": runtime_guidance_text, - "metadata": runtime_guidance_meta, - } - ) - sender( - "user_message", - { - "message": runtime_guidance_text, - "conversation_id": conversation_id, - "runtime_guidance": True, - "runtime_guidance_original": runtime_guidance_text, - "message_source": runtime_guidance_source, - }, + inject_runtime_user_message( + web_terminal=web_terminal, + messages=messages, + text=runtime_guidance_text, + source=runtime_guidance_source, + sender=sender, + conversation_id=conversation_id, + inline=True, ) injected_count += 1 if injected_count: diff --git a/static/src/app/methods/history.ts b/static/src/app/methods/history.ts index b923164..2886daa 100644 --- a/static/src/app/methods/history.ts +++ b/static/src/app/methods/history.ts @@ -261,14 +261,6 @@ export const historyMethods = { debugLog('跳过系统代发的图片/视频消息(仅用于模型查看,不在前端展示)'); return; } - if (message.role === 'user' && isSystemAutoUserMessageMeta(meta)) { - debugLog('跳过系统自动代发的 user 消息(前端不渲染)', { - auto_message_type: meta.auto_message_type || null, - sub_agent_notice: !!meta.sub_agent_notice, - background_command_notice: !!meta.background_command_notice - }); - return; - } if (message.role === 'user') { // 用户消息 - 先结束之前的assistant消息 diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index ba49bc6..6509787 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -1547,15 +1547,14 @@ export const taskPollingMethods = { background_command_notice: !!data?.background_command_notice }); const isAutoUserMessage = isSystemAutoUserMessagePayload(data); - const isRuntimeModeNotice = isRuntimeModeNoticePayload(data); + const incomingImages = Array.isArray(data?.images) ? data.images : []; + const incomingVideos = Array.isArray(data?.videos) ? data.videos : []; + const incomingMediaRefs = Array.isArray(data?.media_refs) + ? data.media_refs + : Array.isArray(data?.mediaRefs) + ? data.mediaRefs + : []; if (!isAutoUserMessage) { - const incomingImages = Array.isArray(data?.images) ? data.images : []; - const incomingVideos = Array.isArray(data?.videos) ? data.videos : []; - const incomingMediaRefs = Array.isArray(data?.media_refs) - ? data.media_refs - : Array.isArray(data?.mediaRefs) - ? data.mediaRefs - : []; const last = getOptimisticUserEchoTarget(this.messages || []); const isLikelyOptimisticEcho = !!( last && @@ -1582,13 +1581,16 @@ export const taskPollingMethods = { this.taskInProgress = true; this.streamingMessage = false; this.stopRequested = false; - } else if (isRuntimeModeNotice) { - this.chatAddUserMessage(message, [], [], [], 'notify'); } else { - debugLog('[TaskPolling] 跳过自动代发 user_message 渲染', { - messagePreview: message.slice(0, 80), - data - }); + // 运行期/空闲期由系统注入的 user 消息(runtime_guidance / sub_agent / background_command / notify 等) + // 现已统一为前缀 [系统通知|...] 的 user 消息,前端按普通 user 渲染。 + this.chatAddUserMessage( + message, + incomingImages, + incomingVideos, + incomingMediaRefs, + resolveUserMessageSource(data) + ); } if (data?.sub_agent_notice) { if (typeof data?.has_running_sub_agents === 'boolean') { @@ -1660,7 +1662,7 @@ export const taskPollingMethods = { this.uiPushToast({ title: '即将重试', - message: `将在 ${retryIn} 秒后重试(第 ${attempt}/${maxAttempts} 次)`, + message: `将在 ${retryIn} 秒后重试(第 ${attempt}/${maxAttempts} 次)\n错误:${data?.message || '未知错误'}`, type: 'info', duration: Math.max(retryIn, 1) * 1000 }); diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index 7973dca..0c0a591 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -682,9 +682,6 @@ const filteredMessages = computed(() => { if (m?.role === 'system') { return false; } - if (isSystemAutoUserMessage(m)) { - return false; - } if (isEmptyAssistantMessage(m)) { userMDebug('ChatArea.filteredMessages:drop-empty-assistant', { actions: Array.isArray(m?.actions) ? m.actions.map((a: any) => a?.type) : [], diff --git a/static/src/utils/chatDisplay.ts b/static/src/utils/chatDisplay.ts index 0bcfa3d..ee38bef 100644 --- a/static/src/utils/chatDisplay.ts +++ b/static/src/utils/chatDisplay.ts @@ -24,8 +24,7 @@ const RUNNING_ANIMATIONS: Record = { terminal_snapshot: 'terminal-animation', todo_create: 'file-animation', todo_update_task: 'file-animation', - create_sub_agent: 'terminal-animation', - wait_sub_agent: 'wait-animation' + create_sub_agent: 'terminal-animation' }; const RUNNING_STATUS_TEXTS: Record = { diff --git a/static/src/utils/icons.ts b/static/src/utils/icons.ts index 7638c65..a05a2b3 100644 --- a/static/src/utils/icons.ts +++ b/static/src/utils/icons.ts @@ -70,7 +70,6 @@ export const TOOL_ICON_MAP = Object.freeze({ terminal_snapshot: 'clipboard', unfocus_file: 'eye', update_memory: 'brain', - wait_sub_agent: 'bot', get_sub_agent_status: 'bot', web_search: 'search', trigger_easter_egg: 'sparkles', diff --git a/utils/tool_result_formatter.py b/utils/tool_result_formatter.py index ccc1e66..4d7489d 100644 --- a/utils/tool_result_formatter.py +++ b/utils/tool_result_formatter.py @@ -1062,7 +1062,6 @@ TOOL_FORMATTERS = { "update_memory": _format_update_memory, "manage_personalization": _format_manage_personalization, "create_sub_agent": _format_create_sub_agent, - "wait_sub_agent": _format_wait_sub_agent, "close_sub_agent": _format_close_sub_agent, "get_sub_agent_status": _format_get_sub_agent_status, }