refactor(messages): 统一运行期消息插入为 [系统通知|source] 前缀的 user 消息
- 新增 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) <noreply@anthropic.com>
This commit is contained in:
parent
30e7709df1
commit
67fdb715f5
@ -95,7 +95,6 @@ python3 -m server.app --path <当前目录> --port 8091 --thinking-mode
|
|||||||
|
|
||||||
工具接口:
|
工具接口:
|
||||||
- `create_sub_agent`: 创建并启动子智能体
|
- `create_sub_agent`: 创建并启动子智能体
|
||||||
- `wait_sub_agent`: 等待子智能体完成并获取结果
|
|
||||||
- `close_sub_agent`: 终止运行中的子智能体
|
- `close_sub_agent`: 终止运行中的子智能体
|
||||||
|
|
||||||
### 🛠️ 丰富的工具集
|
### 🛠️ 丰富的工具集
|
||||||
|
|||||||
@ -38,7 +38,7 @@ description: run_command 工具使用指南。介绍前台与后台模式、后
|
|||||||
|
|
||||||
- timeout 最长 3600 秒。命令在后台继续执行,工具会等约 5 秒收集当前输出并返回,然后 **立即释放控制权**,让你继续处理其他任务。
|
- timeout 最长 3600 秒。命令在后台继续执行,工具会等约 5 秒收集当前输出并返回,然后 **立即释放控制权**,让你继续处理其他任务。
|
||||||
- 返回值里会包含 `command_id`,`status` 可能是 `running_background`。你会拿到已经产生的输出片段,但最终结果要靠系统通知或手动等待。
|
- 返回值里会包含 `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=...)` 等待结果或根据通知定位输出。
|
- 返回的 `command_id` 需要保留,用于后续通过 `sleep(wait_runcommand_id=...)` 等待结果或根据通知定位输出。
|
||||||
|
|
||||||
|
|||||||
@ -122,7 +122,7 @@ create_sub_agent(
|
|||||||
- 你会在对话中看到类似:"这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成..."
|
- 你会在对话中看到类似:"这是一句系统自动发送的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 消息
|
- 不论运行中还是空闲,通知一律是带 `[系统通知|sub_agent]` 前缀的 user 消息;运行中只是通知不触发新一轮回复,空闲时等同于一条新输入会触发回复
|
||||||
- 后台完成由上述系统通知负责,不要再依赖 `wait_sub_agent` 或类似的手动等待流,系统会在能插入消息时自动通知你。
|
- 后台完成由上述系统通知负责,不要再依赖任何手动等待流,系统会在能插入消息时自动通知你。
|
||||||
|
|
||||||
### 模式选择决策树
|
### 模式选择决策树
|
||||||
|
|
||||||
|
|||||||
@ -332,7 +332,17 @@ class MainTerminalCommandMixin:
|
|||||||
)
|
)
|
||||||
system_message = tool_result.get("system_message")
|
system_message = tool_result.get("system_message")
|
||||||
if 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. 在终端显示执行信息(不保存到历史)
|
# 4. 在终端显示执行信息(不保存到历史)
|
||||||
if collected_tool_calls:
|
if collected_tool_calls:
|
||||||
|
|||||||
@ -431,12 +431,6 @@ class MainTerminalContextMixin:
|
|||||||
}
|
}
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
elif conv["role"] == "system" and metadata.get("sub_agent_notice"):
|
|
||||||
# 转换为用户消息,让模型能及时响应
|
|
||||||
messages.append({
|
|
||||||
"role": "user",
|
|
||||||
"content": conv["content"]
|
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
# User 或普通 System 消息
|
# User 或普通 System 消息
|
||||||
images = conv.get("images") or metadata.get("images") or []
|
images = conv.get("images") or metadata.get("images") or []
|
||||||
|
|||||||
@ -164,7 +164,6 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
}
|
}
|
||||||
_SUB_AGENT_SERIES_TOOLS = {
|
_SUB_AGENT_SERIES_TOOLS = {
|
||||||
"create_sub_agent",
|
"create_sub_agent",
|
||||||
"wait_sub_agent",
|
|
||||||
"close_sub_agent",
|
"close_sub_agent",
|
||||||
"terminate_sub_agent",
|
"terminate_sub_agent",
|
||||||
"get_sub_agent_status",
|
"get_sub_agent_status",
|
||||||
@ -198,26 +197,6 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
"terminal_session",
|
"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 = {
|
_READONLY_ALLOWED_EXECUTABLES = {
|
||||||
"grep", "find", "ls", "pwd", "tree", "cat", "head", "tail",
|
"grep", "find", "ls", "pwd", "tree", "cat", "head", "tail",
|
||||||
|
|||||||
@ -75,7 +75,7 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
|||||||
),
|
),
|
||||||
"sub_agent": ToolCategory(
|
"sub_agent": ToolCategory(
|
||||||
label="子智能体",
|
label="子智能体",
|
||||||
tools=["create_sub_agent", "wait_sub_agent", "close_sub_agent"],
|
tools=["create_sub_agent", "close_sub_agent"],
|
||||||
),
|
),
|
||||||
"easter_egg": ToolCategory(
|
"easter_egg": ToolCategory(
|
||||||
label="彩蛋实验",
|
label="彩蛋实验",
|
||||||
|
|||||||
@ -958,7 +958,7 @@ class SubAgentManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def lookup_task(self, *, task_id: Optional[str] = None, agent_id: Optional[int] = None) -> Optional[Dict]:
|
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)
|
task = self._select_task(task_id, agent_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -138,7 +138,7 @@
|
|||||||
## 5. 操作技巧
|
## 5. 操作技巧
|
||||||
|
|
||||||
- **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新
|
- **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新
|
||||||
- **子智能体**:独立子任务并行处理,最多5个同时运行,`wait_sub_agent` 超时需≥创建时设置;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解
|
- **子智能体**:独立子任务并行处理,最多5个同时运行;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解
|
||||||
- **记忆管理**:`update_memory` 支持追加/替换/删除,main 为长期记忆,task 为当前对话记忆
|
- **记忆管理**:`update_memory` 支持追加/替换/删除,main 为长期记忆,task 为当前对话记忆
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -155,7 +155,7 @@
|
|||||||
## 5. 操作技巧
|
## 5. 操作技巧
|
||||||
|
|
||||||
- **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新
|
- **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新
|
||||||
- **子智能体**:独立子任务并行处理,最多5个同时运行,`wait_sub_agent` 超时需≥创建时设置;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解
|
- **子智能体**:独立子任务并行处理,最多5个同时运行;刚开始修改一个项目、需要先找到某个功能模块的修改位置时,优先考虑创建子智能体快速了解
|
||||||
- **记忆管理**:`update_memory` 支持追加/替换/删除,main 为长期记忆,task 为当前对话记忆
|
- **记忆管理**:`update_memory` 支持追加/替换/删除,main 为长期记忆,task 为当前对话记忆
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -53,12 +53,6 @@ def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: st
|
|||||||
notice = str(text or "").strip()
|
notice = str(text or "").strip()
|
||||||
if not notice:
|
if not notice:
|
||||||
return
|
return
|
||||||
metadata = {
|
|
||||||
"runtime_guidance": True,
|
|
||||||
"runtime_guidance_original": notice,
|
|
||||||
"runtime_mode_notice": True,
|
|
||||||
"message_source": "notify",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 若有运行任务,优先入队到该任务(下一轮工具后注入),确保会进入后续 API 请求上下文。
|
# 若有运行任务,优先入队到该任务(下一轮工具后注入),确保会进入后续 API 请求上下文。
|
||||||
try:
|
try:
|
||||||
@ -84,26 +78,25 @@ def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: st
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# 无运行任务时,直接写入当前会话并广播前端显示(来源固定为 notify)。
|
# 无运行任务时,直接写入当前会话并广播前端显示(来源固定为 notify)。
|
||||||
try:
|
from .chat_flow_task_support import inject_runtime_user_message
|
||||||
if getattr(terminal, "context_manager", None):
|
|
||||||
terminal.context_manager.add_conversation("user", notice, metadata=metadata)
|
conversation_id = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None)
|
||||||
except Exception:
|
|
||||||
pass
|
def _emit(event: str, payload: Dict[str, Any]) -> None:
|
||||||
try:
|
try:
|
||||||
socketio.emit(
|
socketio.emit(event, payload, room=f"user_{username}")
|
||||||
"user_message",
|
except Exception:
|
||||||
{
|
pass
|
||||||
"message": notice,
|
|
||||||
"runtime_guidance": True,
|
inject_runtime_user_message(
|
||||||
"runtime_guidance_original": notice,
|
web_terminal=terminal,
|
||||||
"runtime_mode_notice": True,
|
messages=None,
|
||||||
"message_source": "notify",
|
text=notice,
|
||||||
"conversation_id": getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None),
|
source="notify",
|
||||||
},
|
sender=_emit,
|
||||||
room=f"user_{username}",
|
conversation_id=conversation_id,
|
||||||
)
|
inline=True,
|
||||||
except Exception:
|
)
|
||||||
pass
|
|
||||||
|
|
||||||
@chat_bp.route('/api/thinking-mode', methods=['POST'])
|
@chat_bp.route('/api/thinking-mode', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
|||||||
@ -2,43 +2,110 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
from modules.sub_agent_manager import TERMINAL_STATUSES
|
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],
|
web_terminal,
|
||||||
after_tool_call_id: Optional[str],
|
messages: Optional[List[Dict]],
|
||||||
message: str,
|
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,
|
inline: bool,
|
||||||
sender,
|
extra_metadata: Optional[Dict[str, Any]] = None,
|
||||||
metadata: Optional[Dict] = None,
|
persist: bool = True,
|
||||||
sub_agent_notice: bool = False,
|
) -> Optional[str]:
|
||||||
):
|
"""运行期/空闲期统一向对话插入一条 user 消息(带 [系统通知|{source}] 前缀)。
|
||||||
"""统一插入完成通知(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
|
|
||||||
|
|
||||||
insert_role = "system" if inline else "user"
|
- inline=True 表示运行期插入(仅作为通知,不主动触发新一轮回复)。
|
||||||
meta = dict(metadata or {})
|
- inline=False 表示空闲期插入(等价于一条用户消息,由调用方决定是否启动新一轮回复)。
|
||||||
meta["inline"] = inline
|
- messages=None 时仅持久化到对话历史并广播前端(无运行中的 messages 列表可写)。
|
||||||
messages.insert(insert_index, {
|
"""
|
||||||
"role": insert_role,
|
raw = "" if text is None else str(text).strip()
|
||||||
"content": message,
|
if not raw:
|
||||||
"metadata": meta
|
return None
|
||||||
})
|
|
||||||
|
|
||||||
payload = {'content': message, 'inline': inline}
|
src = str(source or "").strip().lower()
|
||||||
if sub_agent_notice:
|
if src not in _VALID_SOURCES:
|
||||||
payload['sub_agent_notice'] = True
|
src = "guidance"
|
||||||
payload.update({k: v for k, v in meta.items() if k in {"command_id", "task_id", "background_command_notice"}})
|
|
||||||
sender('system_message', payload)
|
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):
|
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}")
|
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:
|
if task_id:
|
||||||
web_terminal._announced_sub_agent_tasks.add(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:
|
except Exception as exc:
|
||||||
debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
|
debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
|
||||||
|
|
||||||
# 运行中插入 system 消息,避免触发新的 user 轮次;非运行中保持 user 通知
|
conversation_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None)
|
||||||
if not inline:
|
inject_runtime_user_message(
|
||||||
prefix = "这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成"
|
web_terminal=web_terminal,
|
||||||
if not message.startswith(prefix):
|
|
||||||
message = f"{prefix}\n\n{message}"
|
|
||||||
_insert_completion_notice_message(
|
|
||||||
messages=messages,
|
messages=messages,
|
||||||
after_tool_call_id=after_tool_call_id,
|
text=message,
|
||||||
message=message,
|
source="sub_agent",
|
||||||
inline=inline,
|
|
||||||
sender=sender,
|
sender=sender,
|
||||||
metadata={"sub_agent_notice": True, "task_id": task_id},
|
conversation_id=conversation_id,
|
||||||
sub_agent_notice=True,
|
after_tool_call_id=after_tool_call_id,
|
||||||
|
inline=inline,
|
||||||
|
extra_metadata={"task_id": task_id},
|
||||||
)
|
)
|
||||||
if inline:
|
if inline:
|
||||||
web_terminal._inline_sub_agent_notified.add(inline_key)
|
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}"
|
f"output_len={len(output)} inline={inline}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 与子智能体 inline 通知一致:写入对话历史,确保前端/历史都可见
|
inject_runtime_user_message(
|
||||||
try:
|
web_terminal=web_terminal,
|
||||||
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(
|
|
||||||
messages=messages,
|
messages=messages,
|
||||||
after_tool_call_id=after_tool_call_id,
|
text=message,
|
||||||
message=message,
|
source="background_command",
|
||||||
inline=inline,
|
|
||||||
sender=sender,
|
sender=sender,
|
||||||
metadata={
|
conversation_id=conversation_id,
|
||||||
"background_command_notice": True,
|
after_tool_call_id=after_tool_call_id,
|
||||||
"command_id": command_id,
|
inline=inline,
|
||||||
},
|
extra_metadata={"command_id": command_id},
|
||||||
sub_agent_notice=False,
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
manager.mark_notified(str(command_id))
|
manager.mark_notified(str(command_id))
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from config import TOOL_CALL_COOLDOWN
|
|||||||
from modules.personalization_manager import load_personalization_config, resolve_context_compression_settings
|
from modules.personalization_manager import load_personalization_config, resolve_context_compression_settings
|
||||||
from modules.auto_approval_service import run_auto_approval
|
from modules.auto_approval_service import run_auto_approval
|
||||||
from .deep_compression import run_deep_compression
|
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]]:
|
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,
|
sender=None,
|
||||||
conversation_id: Optional[str] = None,
|
conversation_id: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not isinstance(content, str) or not content.strip():
|
inject_runtime_user_message(
|
||||||
return
|
web_terminal=web_terminal,
|
||||||
text = content.strip()
|
messages=messages,
|
||||||
metadata = {
|
text=content,
|
||||||
"runtime_guidance": True,
|
source="notify",
|
||||||
"runtime_guidance_original": text,
|
sender=sender,
|
||||||
"runtime_mode_notice": True,
|
conversation_id=conversation_id,
|
||||||
"message_source": "notify",
|
inline=True,
|
||||||
}
|
|
||||||
try:
|
|
||||||
web_terminal.context_manager.add_conversation(
|
|
||||||
"user",
|
|
||||||
text,
|
|
||||||
metadata=metadata,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": text,
|
|
||||||
"metadata": metadata,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
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:
|
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
|
action_message = summary
|
||||||
debug_log(f"{function_name} 执行完成: {summary or '无摘要'}")
|
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":
|
if function_name == "sleep":
|
||||||
try:
|
try:
|
||||||
if isinstance(result_data, dict) and result_data.get("mode") == "wait_sub_agent_ids":
|
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:
|
except Exception as exc:
|
||||||
debug_log(f"[ContextCompression] 同步替换本轮 messages 失败: {exc}")
|
debug_log(f"[ContextCompression] 同步替换本轮 messages 失败: {exc}")
|
||||||
debug_log(f"💾 增量保存:工具结果 {function_name}")
|
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
|
||||||
system_message = result_data.get("system_message") if isinstance(result_data, dict) else None
|
if system_message:
|
||||||
if system_message:
|
inject_runtime_user_message(
|
||||||
web_terminal._record_sub_agent_message(system_message, result_data.get("task_id"), inline=False)
|
web_terminal=web_terminal,
|
||||||
maybe_mark_failure_from_message(web_terminal, system_message)
|
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)
|
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:
|
except Exception as exc:
|
||||||
debug_log(f"[RuntimeMode] 应用挂起模式变更失败: {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)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
if tool_failed:
|
if tool_failed:
|
||||||
mark_force_thinking(web_terminal, reason=f"{function_name}_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 都完成后再注入,
|
# 运行期模式通知:必须等待同一轮全部 tool_call 都完成后再注入,
|
||||||
# 避免在 assistant.tool_calls 与对应 tool 消息之间插入 system 消息导致 API 报错。
|
# 避免在 assistant.tool_calls 与对应 tool 消息之间插入 user 消息导致 API 报错。
|
||||||
if pending_runtime_mode_notices:
|
if pending_runtime_mode_notices:
|
||||||
for notice in pending_runtime_mode_notices:
|
for notice in pending_runtime_mode_notices:
|
||||||
_inject_runtime_mode_notice(
|
_inject_runtime_mode_notice(
|
||||||
@ -1222,39 +1209,16 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
runtime_guidance_text = str(raw_item or "").strip()
|
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:
|
if not runtime_guidance_text:
|
||||||
continue
|
continue
|
||||||
runtime_guidance_meta = {
|
inject_runtime_user_message(
|
||||||
"runtime_guidance": True,
|
web_terminal=web_terminal,
|
||||||
"runtime_guidance_original": runtime_guidance_text,
|
messages=messages,
|
||||||
"message_source": runtime_guidance_source,
|
text=runtime_guidance_text,
|
||||||
}
|
source=runtime_guidance_source,
|
||||||
try:
|
sender=sender,
|
||||||
web_terminal.context_manager.add_conversation(
|
conversation_id=conversation_id,
|
||||||
"user",
|
inline=True,
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
injected_count += 1
|
injected_count += 1
|
||||||
if injected_count:
|
if injected_count:
|
||||||
|
|||||||
@ -261,14 +261,6 @@ export const historyMethods = {
|
|||||||
debugLog('跳过系统代发的图片/视频消息(仅用于模型查看,不在前端展示)');
|
debugLog('跳过系统代发的图片/视频消息(仅用于模型查看,不在前端展示)');
|
||||||
return;
|
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') {
|
if (message.role === 'user') {
|
||||||
// 用户消息 - 先结束之前的assistant消息
|
// 用户消息 - 先结束之前的assistant消息
|
||||||
|
|||||||
@ -1547,15 +1547,14 @@ export const taskPollingMethods = {
|
|||||||
background_command_notice: !!data?.background_command_notice
|
background_command_notice: !!data?.background_command_notice
|
||||||
});
|
});
|
||||||
const isAutoUserMessage = isSystemAutoUserMessagePayload(data);
|
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) {
|
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 last = getOptimisticUserEchoTarget(this.messages || []);
|
||||||
const isLikelyOptimisticEcho = !!(
|
const isLikelyOptimisticEcho = !!(
|
||||||
last &&
|
last &&
|
||||||
@ -1582,13 +1581,16 @@ export const taskPollingMethods = {
|
|||||||
this.taskInProgress = true;
|
this.taskInProgress = true;
|
||||||
this.streamingMessage = false;
|
this.streamingMessage = false;
|
||||||
this.stopRequested = false;
|
this.stopRequested = false;
|
||||||
} else if (isRuntimeModeNotice) {
|
|
||||||
this.chatAddUserMessage(message, [], [], [], 'notify');
|
|
||||||
} else {
|
} else {
|
||||||
debugLog('[TaskPolling] 跳过自动代发 user_message 渲染', {
|
// 运行期/空闲期由系统注入的 user 消息(runtime_guidance / sub_agent / background_command / notify 等)
|
||||||
messagePreview: message.slice(0, 80),
|
// 现已统一为前缀 [系统通知|...] 的 user 消息,前端按普通 user 渲染。
|
||||||
data
|
this.chatAddUserMessage(
|
||||||
});
|
message,
|
||||||
|
incomingImages,
|
||||||
|
incomingVideos,
|
||||||
|
incomingMediaRefs,
|
||||||
|
resolveUserMessageSource(data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (data?.sub_agent_notice) {
|
if (data?.sub_agent_notice) {
|
||||||
if (typeof data?.has_running_sub_agents === 'boolean') {
|
if (typeof data?.has_running_sub_agents === 'boolean') {
|
||||||
@ -1660,7 +1662,7 @@ export const taskPollingMethods = {
|
|||||||
|
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '即将重试',
|
title: '即将重试',
|
||||||
message: `将在 ${retryIn} 秒后重试(第 ${attempt}/${maxAttempts} 次)`,
|
message: `将在 ${retryIn} 秒后重试(第 ${attempt}/${maxAttempts} 次)\n错误:${data?.message || '未知错误'}`,
|
||||||
type: 'info',
|
type: 'info',
|
||||||
duration: Math.max(retryIn, 1) * 1000
|
duration: Math.max(retryIn, 1) * 1000
|
||||||
});
|
});
|
||||||
|
|||||||
@ -682,9 +682,6 @@ const filteredMessages = computed(() => {
|
|||||||
if (m?.role === 'system') {
|
if (m?.role === 'system') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isSystemAutoUserMessage(m)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isEmptyAssistantMessage(m)) {
|
if (isEmptyAssistantMessage(m)) {
|
||||||
userMDebug('ChatArea.filteredMessages:drop-empty-assistant', {
|
userMDebug('ChatArea.filteredMessages:drop-empty-assistant', {
|
||||||
actions: Array.isArray(m?.actions) ? m.actions.map((a: any) => a?.type) : [],
|
actions: Array.isArray(m?.actions) ? m.actions.map((a: any) => a?.type) : [],
|
||||||
|
|||||||
@ -24,8 +24,7 @@ const RUNNING_ANIMATIONS: Record<string, string> = {
|
|||||||
terminal_snapshot: 'terminal-animation',
|
terminal_snapshot: 'terminal-animation',
|
||||||
todo_create: 'file-animation',
|
todo_create: 'file-animation',
|
||||||
todo_update_task: 'file-animation',
|
todo_update_task: 'file-animation',
|
||||||
create_sub_agent: 'terminal-animation',
|
create_sub_agent: 'terminal-animation'
|
||||||
wait_sub_agent: 'wait-animation'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RUNNING_STATUS_TEXTS: Record<string, string> = {
|
const RUNNING_STATUS_TEXTS: Record<string, string> = {
|
||||||
|
|||||||
@ -70,7 +70,6 @@ export const TOOL_ICON_MAP = Object.freeze({
|
|||||||
terminal_snapshot: 'clipboard',
|
terminal_snapshot: 'clipboard',
|
||||||
unfocus_file: 'eye',
|
unfocus_file: 'eye',
|
||||||
update_memory: 'brain',
|
update_memory: 'brain',
|
||||||
wait_sub_agent: 'bot',
|
|
||||||
get_sub_agent_status: 'bot',
|
get_sub_agent_status: 'bot',
|
||||||
web_search: 'search',
|
web_search: 'search',
|
||||||
trigger_easter_egg: 'sparkles',
|
trigger_easter_egg: 'sparkles',
|
||||||
|
|||||||
@ -1062,7 +1062,6 @@ TOOL_FORMATTERS = {
|
|||||||
"update_memory": _format_update_memory,
|
"update_memory": _format_update_memory,
|
||||||
"manage_personalization": _format_manage_personalization,
|
"manage_personalization": _format_manage_personalization,
|
||||||
"create_sub_agent": _format_create_sub_agent,
|
"create_sub_agent": _format_create_sub_agent,
|
||||||
"wait_sub_agent": _format_wait_sub_agent,
|
|
||||||
"close_sub_agent": _format_close_sub_agent,
|
"close_sub_agent": _format_close_sub_agent,
|
||||||
"get_sub_agent_status": _format_get_sub_agent_status,
|
"get_sub_agent_status": _format_get_sub_agent_status,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user