fix(sub_agent): 移除 search_workspace 工具并清理内存调试代码
- 删除子智能体 search_workspace 工具定义、路由、formatter 与测试 - 传统/多智能体模式共用 SUB_AGENT_TOOLS,一处删除同时生效 - 老对话恢复后再调用 search_workspace 会返回 未知工具 错误 - 清理多智能体排查期间加入的 memory_debug 模块及所有调用点 (modules/memory_debug.py 删除,server/ core/ utils/ modules/sub_agent/ 中相关埋点全部移除) - 顺手删除 SubAgentActivityDialog 中 search_workspace 的历史显示文案
This commit is contained in:
parent
2b131de702
commit
d4c3e73134
@ -80,7 +80,6 @@ from utils.context_manager import ContextManager, AUTO_SHALLOW_PLACEHOLDER
|
|||||||
from utils.host_workspace_debug import write_host_workspace_debug
|
from utils.host_workspace_debug import write_host_workspace_debug
|
||||||
from utils.tool_result_formatter import format_tool_result_for_context
|
from utils.tool_result_formatter import format_tool_result_for_context
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
from modules.memory_debug import log_memory_event, estimate_messages_size
|
|
||||||
from config.model_profiles import (
|
from config.model_profiles import (
|
||||||
get_model_profile,
|
get_model_profile,
|
||||||
get_model_prompt_replacements,
|
get_model_prompt_replacements,
|
||||||
@ -98,12 +97,6 @@ class MessagesMixin:
|
|||||||
|
|
||||||
def build_messages(self, context: Dict, user_input: str) -> List[Dict]:
|
def build_messages(self, context: Dict, user_input: str) -> List[Dict]:
|
||||||
"""构建消息列表(添加终端内容注入)"""
|
"""构建消息列表(添加终端内容注入)"""
|
||||||
log_memory_event(
|
|
||||||
"build_messages_enter",
|
|
||||||
terminal_id=id(self),
|
|
||||||
conversation_id=getattr(getattr(self, "context_manager", None), "current_conversation_id", None),
|
|
||||||
conversation_size=estimate_messages_size(context.get("conversation")),
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
file_tree_preview = (context.get("project_info", {}).get("file_tree") or "").splitlines()
|
file_tree_preview = (context.get("project_info", {}).get("file_tree") or "").splitlines()
|
||||||
write_host_workspace_debug(
|
write_host_workspace_debug(
|
||||||
@ -423,12 +416,4 @@ class MessagesMixin:
|
|||||||
print(f"[ContextCompression] build_messages 替换tool占位符: {replaced_tool_count} 条")
|
print(f"[ContextCompression] build_messages 替换tool占位符: {replaced_tool_count} 条")
|
||||||
if deep_compacted_skipped:
|
if deep_compacted_skipped:
|
||||||
print(f"[ContextCompression] build_messages 跳过已深压缩消息: {deep_compacted_skipped} 条")
|
print(f"[ContextCompression] build_messages 跳过已深压缩消息: {deep_compacted_skipped} 条")
|
||||||
log_memory_event(
|
|
||||||
"build_messages_exit",
|
|
||||||
terminal_id=id(self),
|
|
||||||
conversation_id=getattr(getattr(self, "context_manager", None), "current_conversation_id", None),
|
|
||||||
conversation_size=estimate_messages_size(context.get("conversation")),
|
|
||||||
result_messages_size=estimate_messages_size(messages),
|
|
||||||
deep_compacted_skipped=deep_compacted_skipped,
|
|
||||||
)
|
|
||||||
return messages
|
return messages
|
||||||
|
|||||||
@ -20,7 +20,6 @@ except ImportError:
|
|||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
|
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
|
||||||
from modules.terminal_manager import TerminalManager
|
from modules.terminal_manager import TerminalManager
|
||||||
from modules.memory_debug import start_periodic_sampling
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from modules.user_container_manager import ContainerHandle
|
from modules.user_container_manager import ContainerHandle
|
||||||
@ -102,9 +101,6 @@ class WebTerminal(MainTerminal):
|
|||||||
print(f"[WebTerminal] 实时token统计已启用")
|
print(f"[WebTerminal] 实时token统计已启用")
|
||||||
else:
|
else:
|
||||||
print(f"[WebTerminal] 警告:message_callback为None,无法启用实时token统计")
|
print(f"[WebTerminal] 警告:message_callback为None,无法启用实时token统计")
|
||||||
|
|
||||||
# 启动后台内存采样,帮助定位偶发内存暴涨
|
|
||||||
start_periodic_sampling()
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 新增:对话管理相关方法(Web版本)
|
# 新增:对话管理相关方法(Web版本)
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@ -1,336 +0,0 @@
|
|||||||
"""内存调试工具:在关键路径记录内存变化,帮助定位暴涨点。
|
|
||||||
|
|
||||||
使用方式:
|
|
||||||
from modules.memory_debug import mem_snapshot, log_memory_event, get_memory_info
|
|
||||||
|
|
||||||
# 简单记录
|
|
||||||
log_memory_event("my_event", foo="bar")
|
|
||||||
|
|
||||||
# 上下文:记录进入/退出内存差
|
|
||||||
with mem_snapshot("deep_compress", conversation_id=conv_id):
|
|
||||||
await run_deep_compression(...)
|
|
||||||
|
|
||||||
输出:
|
|
||||||
~/.astrion/astrion/<mode>/logs/memory_debug.log(JSONL)
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import tracemalloc
|
|
||||||
from contextlib import asynccontextmanager, contextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, AsyncIterator, Dict, Optional
|
|
||||||
|
|
||||||
from utils.log_rotation import append_line
|
|
||||||
|
|
||||||
# 开关:默认关闭,避免生产环境大量写日志。
|
|
||||||
# 用户需要排查时设置环境变量 ASTRION_MEMORY_DEBUG=1 开启。
|
|
||||||
ENABLED = str(os.environ.get("ASTRION_MEMORY_DEBUG", "1")).strip().lower() in {
|
|
||||||
"1", "true", "yes", "on",
|
|
||||||
}
|
|
||||||
|
|
||||||
_log_lock = threading.Lock()
|
|
||||||
_log_path: Optional[Path] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_log_path() -> Path:
|
|
||||||
"""解析内存调试日志路径,复用项目运行态 logs 目录。"""
|
|
||||||
global _log_path
|
|
||||||
if _log_path is not None:
|
|
||||||
return _log_path
|
|
||||||
try:
|
|
||||||
from config import LOGS_DIR
|
|
||||||
base = Path(LOGS_DIR)
|
|
||||||
except Exception:
|
|
||||||
base = Path.home() / ".astrion" / "astrion" / "host" / "logs"
|
|
||||||
_log_path = base / "memory_debug.log"
|
|
||||||
return _log_path
|
|
||||||
|
|
||||||
|
|
||||||
_peak_rss_mb: Optional[float] = None
|
|
||||||
_last_sample_rss_mb: Optional[float] = None
|
|
||||||
_sample_thread: Optional[threading.Thread] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _current_rss_mb() -> Optional[float]:
|
|
||||||
"""当前进程 RSS(MB),优先使用 psutil(实时),失败回退到 resource。"""
|
|
||||||
global _peak_rss_mb
|
|
||||||
try:
|
|
||||||
import psutil
|
|
||||||
proc = psutil.Process()
|
|
||||||
rss = proc.memory_info().rss / (1024 * 1024)
|
|
||||||
if _peak_rss_mb is None or rss > _peak_rss_mb:
|
|
||||||
_peak_rss_mb = rss
|
|
||||||
return rss
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
import resource
|
|
||||||
usage = resource.getrusage(resource.RUSAGE_SELF)
|
|
||||||
# macOS: ru_maxrss 单位是 bytes;Linux: 单位是 KB
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
rss = usage.ru_maxrss / (1024 * 1024)
|
|
||||||
else:
|
|
||||||
rss = usage.ru_maxrss / 1024.0
|
|
||||||
if _peak_rss_mb is None or rss > _peak_rss_mb:
|
|
||||||
_peak_rss_mb = rss
|
|
||||||
return rss
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _current_vms_mb() -> Optional[float]:
|
|
||||||
"""当前进程 VMS(MB),失败返回 None。"""
|
|
||||||
try:
|
|
||||||
import psutil
|
|
||||||
proc = psutil.Process()
|
|
||||||
return proc.memory_info().vms / (1024 * 1024)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_detailed_memory_info() -> Dict[str, Optional[float]]:
|
|
||||||
"""获取更详细的内存指标,更接近 macOS 活动监视器。"""
|
|
||||||
result: Dict[str, Optional[float]] = {
|
|
||||||
"rss_mb": _current_rss_mb(),
|
|
||||||
"vms_mb": _current_vms_mb(),
|
|
||||||
"uss_mb": None,
|
|
||||||
"shared_mb": None,
|
|
||||||
"private_mb": None,
|
|
||||||
"data_mb": None,
|
|
||||||
"dirty_mb": None,
|
|
||||||
"compressed_mb": None,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
import psutil
|
|
||||||
proc = psutil.Process()
|
|
||||||
info = proc.memory_info()
|
|
||||||
# uss 只在 memory_full_info() 中提供
|
|
||||||
try:
|
|
||||||
full = proc.memory_full_info()
|
|
||||||
result["uss_mb"] = getattr(full, "uss", None) / (1024 * 1024) if getattr(full, "uss", None) else None
|
|
||||||
result["compressed_mb"] = getattr(full, "compressed", None) / (1024 * 1024) if getattr(full, "compressed", None) else None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
result["shared_mb"] = getattr(info, "shared", None) / (1024 * 1024) if getattr(info, "shared", None) else None
|
|
||||||
result["private_mb"] = getattr(info, "private", None) / (1024 * 1024) if getattr(info, "private", None) else None
|
|
||||||
result["data_mb"] = getattr(info, "data", None) / (1024 * 1024) if getattr(info, "data", None) else None
|
|
||||||
result["dirty_mb"] = getattr(info, "dirty", None) / (1024 * 1024) if getattr(info, "dirty", None) else None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_memory_info() -> Dict[str, Optional[float]]:
|
|
||||||
return _get_detailed_memory_info()
|
|
||||||
|
|
||||||
|
|
||||||
def _estimate_text_size(text: Any) -> int:
|
|
||||||
"""估算文本/列表/字典在 JSON 序列化后的大小(字符数)。"""
|
|
||||||
try:
|
|
||||||
return len(json.dumps(text, ensure_ascii=False))
|
|
||||||
except Exception:
|
|
||||||
return len(str(text))
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_messages_size(messages: Any) -> Dict[str, Any]:
|
|
||||||
"""估算消息列表的规模和体积。"""
|
|
||||||
if not messages:
|
|
||||||
return {"count": 0, "total_chars": 0, "avg_chars": 0}
|
|
||||||
if not isinstance(messages, list):
|
|
||||||
return {"count": 0, "total_chars": _estimate_text_size(messages), "avg_chars": 0}
|
|
||||||
total = 0
|
|
||||||
for m in messages:
|
|
||||||
total += _estimate_text_size(m)
|
|
||||||
return {
|
|
||||||
"count": len(messages),
|
|
||||||
"total_chars": total,
|
|
||||||
"avg_chars": total // max(len(messages), 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def log_memory_event(event: str, **kwargs: Any) -> None:
|
|
||||||
"""写一条内存调试日志。"""
|
|
||||||
if not ENABLED:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
info = get_memory_info()
|
|
||||||
payload: Dict[str, Any] = {
|
|
||||||
"t": time.time(),
|
|
||||||
"event": event,
|
|
||||||
"rss_mb": info.get("rss_mb"),
|
|
||||||
"vms_mb": info.get("vms_mb"),
|
|
||||||
}
|
|
||||||
# 过滤掉不可序列化的对象
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
try:
|
|
||||||
json.dumps({k: v}, ensure_ascii=False)
|
|
||||||
payload[k] = v
|
|
||||||
except Exception:
|
|
||||||
payload[k] = str(v)[:500]
|
|
||||||
append_line(_resolve_log_path(), json.dumps(payload, ensure_ascii=False))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def mem_snapshot(event: str, **kwargs: Any):
|
|
||||||
"""同步上下文管理器:记录进入和退出时的内存变化。"""
|
|
||||||
if not ENABLED:
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
start_info = get_memory_info()
|
|
||||||
start_t = time.time()
|
|
||||||
log_memory_event(f"{event}_enter", duration_ms=0, **kwargs)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
end_info = get_memory_info()
|
|
||||||
delta_rss = None
|
|
||||||
delta_vms = None
|
|
||||||
if start_info.get("rss_mb") is not None and end_info.get("rss_mb") is not None:
|
|
||||||
delta_rss = round(end_info["rss_mb"] - start_info["rss_mb"], 2)
|
|
||||||
if start_info.get("vms_mb") is not None and end_info.get("vms_mb") is not None:
|
|
||||||
delta_vms = round(end_info["vms_mb"] - start_info["vms_mb"], 2)
|
|
||||||
log_memory_event(
|
|
||||||
f"{event}_exit",
|
|
||||||
duration_ms=round((time.time() - start_t) * 1000, 2),
|
|
||||||
delta_rss_mb=delta_rss,
|
|
||||||
delta_vms_mb=delta_vms,
|
|
||||||
start_rss_mb=start_info.get("rss_mb"),
|
|
||||||
end_rss_mb=end_info.get("rss_mb"),
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def async_mem_snapshot(event: str, **kwargs: Any) -> AsyncIterator[None]:
|
|
||||||
"""异步上下文管理器:记录进入和退出时的内存变化。"""
|
|
||||||
if not ENABLED:
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
start_info = get_memory_info()
|
|
||||||
start_t = time.time()
|
|
||||||
log_memory_event(f"{event}_enter", duration_ms=0, **kwargs)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
end_info = get_memory_info()
|
|
||||||
delta_rss = None
|
|
||||||
delta_vms = None
|
|
||||||
if start_info.get("rss_mb") is not None and end_info.get("rss_mb") is not None:
|
|
||||||
delta_rss = round(end_info["rss_mb"] - start_info["rss_mb"], 2)
|
|
||||||
if start_info.get("vms_mb") is not None and end_info.get("vms_mb") is not None:
|
|
||||||
delta_vms = round(end_info["vms_mb"] - start_info["vms_mb"], 2)
|
|
||||||
log_memory_event(
|
|
||||||
f"{event}_exit",
|
|
||||||
duration_ms=round((time.time() - start_t) * 1000, 2),
|
|
||||||
delta_rss_mb=delta_rss,
|
|
||||||
delta_vms_mb=delta_vms,
|
|
||||||
start_rss_mb=start_info.get("rss_mb"),
|
|
||||||
end_rss_mb=end_info.get("rss_mb"),
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TopAllocator:
|
|
||||||
"""基于 tracemalloc 的 Top 分配点采样(开销较大,按需显式调用)。"""
|
|
||||||
|
|
||||||
def __init__(self, top_n: int = 10):
|
|
||||||
self.top_n = top_n
|
|
||||||
|
|
||||||
def start(self) -> None:
|
|
||||||
try:
|
|
||||||
tracemalloc.start()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def snapshot(self, label: str = "") -> None:
|
|
||||||
try:
|
|
||||||
snap = tracemalloc.take_snapshot()
|
|
||||||
top = snap.statistics("lineno")[: self.top_n]
|
|
||||||
lines = []
|
|
||||||
for stat in top:
|
|
||||||
lines.append(
|
|
||||||
f"{stat.size / (1024 * 1024):.2f}MB {stat.count} {stat.traceback.format()[-1]}"
|
|
||||||
)
|
|
||||||
log_memory_event(
|
|
||||||
"tracemalloc_top",
|
|
||||||
label=label,
|
|
||||||
top=lines,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
try:
|
|
||||||
tracemalloc.stop()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _periodic_sampling(
|
|
||||||
interval_seconds: float = 0.1,
|
|
||||||
spike_threshold_mb: float = 50.0,
|
|
||||||
max_records: int = 10,
|
|
||||||
) -> None:
|
|
||||||
"""后台线程:每 100ms 采样一次内存,发现快速增长或峰值时记录。"""
|
|
||||||
global _last_sample_rss_mb, _peak_rss_mb
|
|
||||||
records_since_spike = 0
|
|
||||||
last_regular_log = 0.0
|
|
||||||
while ENABLED:
|
|
||||||
try:
|
|
||||||
info = get_memory_info()
|
|
||||||
rss = info.get("rss_mb")
|
|
||||||
if rss is not None:
|
|
||||||
spike = False
|
|
||||||
delta = None
|
|
||||||
if _last_sample_rss_mb is not None:
|
|
||||||
delta = rss - _last_sample_rss_mb
|
|
||||||
if delta >= spike_threshold_mb:
|
|
||||||
spike = True
|
|
||||||
records_since_spike = max_records
|
|
||||||
_last_sample_rss_mb = rss
|
|
||||||
if records_since_spike > 0:
|
|
||||||
records_since_spike -= 1
|
|
||||||
log_memory_event(
|
|
||||||
"memory_periodic_spike",
|
|
||||||
**info,
|
|
||||||
delta_from_last_mb=delta,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
now = time.time()
|
|
||||||
if now - last_regular_log >= 2.0:
|
|
||||||
last_regular_log = now
|
|
||||||
log_memory_event(
|
|
||||||
"memory_periodic_sample",
|
|
||||||
**info,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(interval_seconds)
|
|
||||||
|
|
||||||
|
|
||||||
def start_periodic_sampling(
|
|
||||||
interval_seconds: float = 0.1,
|
|
||||||
spike_threshold_mb: float = 50.0,
|
|
||||||
) -> None:
|
|
||||||
"""启动后台内存采样线程。幂等:重复调用不会启动多个线程。"""
|
|
||||||
global _sample_thread
|
|
||||||
if not ENABLED:
|
|
||||||
return
|
|
||||||
if _sample_thread is not None and _sample_thread.is_alive():
|
|
||||||
return
|
|
||||||
_sample_thread = threading.Thread(
|
|
||||||
target=_periodic_sampling,
|
|
||||||
args=(interval_seconds, spike_threshold_mb),
|
|
||||||
name="memory-debug-sampler",
|
|
||||||
daemon=True,
|
|
||||||
)
|
|
||||||
_sample_thread.start()
|
|
||||||
@ -24,12 +24,11 @@ from config import (
|
|||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
from modules.sub_agent.task import SubAgentTask
|
from modules.sub_agent.task import SubAgentTask
|
||||||
from modules.sub_agent.prompts import build_user_message, build_system_prompt
|
from modules.sub_agent.prompts import build_user_message, build_system_prompt
|
||||||
from modules.sub_agent.tools import handle_search_workspace, handle_read_mediafile
|
from modules.sub_agent.tools import handle_read_mediafile
|
||||||
from modules.sub_agent.state import SubAgentStateMixin
|
from modules.sub_agent.state import SubAgentStateMixin
|
||||||
from modules.sub_agent.stats import SubAgentStatsMixin
|
from modules.sub_agent.stats import SubAgentStatsMixin
|
||||||
from modules.sub_agent.creation import SubAgentCreationMixin
|
from modules.sub_agent.creation import SubAgentCreationMixin
|
||||||
from modules.multi_agent.debug_logger import ma_debug
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
from modules.memory_debug import log_memory_event
|
|
||||||
from server.utils_common import debug_log
|
from server.utils_common import debug_log
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -285,16 +284,6 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
state_id=id(multi_agent_state) if multi_agent_state else None,
|
state_id=id(multi_agent_state) if multi_agent_state else None,
|
||||||
)
|
)
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_manager_create_sub_agent",
|
|
||||||
task_id=task_id,
|
|
||||||
agent_id=agent_id,
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
display_name=display_name,
|
|
||||||
multi_agent_mode=multi_agent_mode,
|
|
||||||
total_tasks=len(self.tasks),
|
|
||||||
running_tasks=len(self._running_tasks),
|
|
||||||
)
|
|
||||||
task_coro = sub_agent.run()
|
task_coro = sub_agent.run()
|
||||||
asyncio_task = self._run_coro(task_coro)
|
asyncio_task = self._run_coro(task_coro)
|
||||||
sub_agent._task = asyncio_task
|
sub_agent._task = asyncio_task
|
||||||
@ -304,13 +293,6 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
|
|
||||||
def _on_done(fut):
|
def _on_done(fut):
|
||||||
try:
|
try:
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_task_done_callback",
|
|
||||||
task_id=task_id,
|
|
||||||
agent_id=agent_id,
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
multi_agent_mode=multi_agent_mode,
|
|
||||||
)
|
|
||||||
self._running_tasks.pop(task_id, None)
|
self._running_tasks.pop(task_id, None)
|
||||||
# 多智能体模式下 failed 视为可复活状态,保留实例引用供后续 send_message_to_sub_agent 重新激活
|
# 多智能体模式下 failed 视为可复活状态,保留实例引用供后续 send_message_to_sub_agent 重新激活
|
||||||
if multi_agent_mode:
|
if multi_agent_mode:
|
||||||
@ -711,18 +693,11 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
try:
|
try:
|
||||||
# 多智能体模式常见问答工具已在 SubAgentTask._execute_multi_agent_tool 中处理
|
# 多智能体模式常见问答工具已在 SubAgentTask._execute_multi_agent_tool 中处理
|
||||||
# 这里只处理实际通过主进程执行的工具
|
# 这里只处理实际通过主进程执行的工具
|
||||||
if tool_name == "search_workspace":
|
|
||||||
return await handle_search_workspace(self.project_path, self.terminal, arguments)
|
|
||||||
if tool_name == "read_mediafile":
|
if tool_name == "read_mediafile":
|
||||||
return await handle_read_mediafile(self.project_path, arguments)
|
return await handle_read_mediafile(self.project_path, arguments)
|
||||||
|
|
||||||
# 其余工具直接走主进程 handle_tool_call,自然经过沙箱/容器/权限链路
|
# 其余工具直接走主进程 handle_tool_call,自然经过沙箱/容器/权限链路
|
||||||
result_text = await self.terminal.handle_tool_call(tool_name, arguments)
|
result_text = await self.terminal.handle_tool_call(tool_name, arguments)
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_tool_result",
|
|
||||||
tool_name=tool_name,
|
|
||||||
result_chars=len(result_text),
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
return json.loads(result_text)
|
return json.loads(result_text)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -766,27 +741,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log_memory_event(
|
conversation_data = json.loads(conversation_file.read_text(encoding="utf-8"))
|
||||||
"sub_agent_restore_load_conversation_start",
|
|
||||||
task_id=task_id,
|
|
||||||
agent_id=task.get("agent_id"),
|
|
||||||
conversation_file=str(conversation_file),
|
|
||||||
)
|
|
||||||
conversation_text = conversation_file.read_text(encoding="utf-8")
|
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_restore_after_read",
|
|
||||||
task_id=task_id,
|
|
||||||
agent_id=task.get("agent_id"),
|
|
||||||
conversation_file_chars=len(conversation_text),
|
|
||||||
)
|
|
||||||
conversation_data = json.loads(conversation_text)
|
|
||||||
messages = list(conversation_data.get("messages") or [])
|
messages = list(conversation_data.get("messages") or [])
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_restore_after_parse",
|
|
||||||
task_id=task_id,
|
|
||||||
agent_id=task.get("agent_id"),
|
|
||||||
messages_count=len(messages),
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"[restore] 读取任务 {task_id} 对话文件失败: {exc}")
|
logger.warning(f"[restore] 读取任务 {task_id} 对话文件失败: {exc}")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -28,7 +28,6 @@ if TYPE_CHECKING:
|
|||||||
from modules.multi_agent.state import MultiAgentState
|
from modules.multi_agent.state import MultiAgentState
|
||||||
|
|
||||||
from modules.multi_agent.debug_logger import ma_debug
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
from modules.memory_debug import async_mem_snapshot, log_memory_event, estimate_messages_size
|
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
@ -121,15 +120,6 @@ class SubAgentTask:
|
|||||||
self._pending_answer_question_id: Optional[str] = None
|
self._pending_answer_question_id: Optional[str] = None
|
||||||
self._answered_question_ids: Set[str] = set()
|
self._answered_question_ids: Set[str] = set()
|
||||||
|
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_task_created",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
multi_agent_mode=self.multi_agent_mode,
|
|
||||||
initial_messages_size=estimate_messages_size(self.messages),
|
|
||||||
)
|
|
||||||
|
|
||||||
def emit(self, type_: str, data: Dict[str, Any]) -> None:
|
def emit(self, type_: str, data: Dict[str, Any]) -> None:
|
||||||
"""输出一行 JSONL 到 progress 文件并缓存。"""
|
"""输出一行 JSONL 到 progress 文件并缓存。"""
|
||||||
line = json.dumps({"type": type_, **data}, ensure_ascii=False)
|
line = json.dumps({"type": type_, **data}, ensure_ascii=False)
|
||||||
@ -273,30 +263,13 @@ class SubAgentTask:
|
|||||||
pending_answer_question_id=self._pending_answer_question_id,
|
pending_answer_question_id=self._pending_answer_question_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async with async_mem_snapshot(
|
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
||||||
"sub_agent_call_model",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
turn=turn,
|
|
||||||
messages_size=estimate_messages_size(self.messages),
|
|
||||||
):
|
|
||||||
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
|
||||||
if usage:
|
if usage:
|
||||||
self._apply_usage(usage)
|
self._apply_usage(usage)
|
||||||
|
|
||||||
# 上下文压缩检查:超过阈值时触发深度压缩
|
# 上下文压缩检查:超过阈值时触发深度压缩
|
||||||
if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens:
|
if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens:
|
||||||
with mem_snapshot(
|
compressed = self._deep_compress_messages()
|
||||||
"sub_agent_deep_compress",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
turn=turn,
|
|
||||||
messages_size=estimate_messages_size(self.messages),
|
|
||||||
current_context_tokens=self.current_context_tokens,
|
|
||||||
):
|
|
||||||
compressed = self._deep_compress_messages()
|
|
||||||
if compressed:
|
if compressed:
|
||||||
ma_debug(
|
ma_debug(
|
||||||
"sub_agent_deep_compressed",
|
"sub_agent_deep_compressed",
|
||||||
@ -414,15 +387,6 @@ class SubAgentTask:
|
|||||||
output_len=len(output_text),
|
output_len=len(output_text),
|
||||||
is_final=is_final,
|
is_final=is_final,
|
||||||
)
|
)
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_forward_output",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
output_len=len(output_text),
|
|
||||||
is_final=is_final,
|
|
||||||
messages_size=estimate_messages_size(self.messages),
|
|
||||||
)
|
|
||||||
if not self.multi_agent_state:
|
if not self.multi_agent_state:
|
||||||
ma_debug("sub_agent_forward_no_state", task_id=self.task_id, agent_id=self.agent_id)
|
ma_debug("sub_agent_forward_no_state", task_id=self.task_id, agent_id=self.agent_id)
|
||||||
return
|
return
|
||||||
@ -638,15 +602,6 @@ class SubAgentTask:
|
|||||||
if chunk.get("usage"):
|
if chunk.get("usage"):
|
||||||
usage = chunk["usage"]
|
usage = chunk["usage"]
|
||||||
|
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_call_model_assembled",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
assistant_message_chars=len(assistant_message),
|
|
||||||
reasoning_chars=len(reasoning),
|
|
||||||
tool_calls_count=len(tool_calls),
|
|
||||||
)
|
|
||||||
return assistant_message, reasoning, tool_calls, usage
|
return assistant_message, reasoning, tool_calls, usage
|
||||||
|
|
||||||
def _parse_args(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
def _parse_args(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@ -662,19 +617,11 @@ class SubAgentTask:
|
|||||||
多智能体模式下,对于通信工具(ask_master / ask_other_agent / answer_other_agent/
|
多智能体模式下,对于通信工具(ask_master / ask_other_agent / answer_other_agent/
|
||||||
list_active_sub_agents)在主进程内直接处理,不再转发到 WebTerminal。
|
list_active_sub_agents)在主进程内直接处理,不再转发到 WebTerminal。
|
||||||
"""
|
"""
|
||||||
async with async_mem_snapshot(
|
if self.multi_agent_mode and self.multi_agent_state:
|
||||||
"sub_agent_execute_tool",
|
result = await self._execute_multi_agent_tool(name, args)
|
||||||
task_id=self.task_id,
|
if result is not None:
|
||||||
agent_id=self.agent_id,
|
return result
|
||||||
display_name=self.display_name,
|
return await self.manager.execute_tool_for_sub_agent(name, args)
|
||||||
tool_name=name,
|
|
||||||
messages_size=estimate_messages_size(self.messages),
|
|
||||||
):
|
|
||||||
if self.multi_agent_mode and self.multi_agent_state:
|
|
||||||
result = await self._execute_multi_agent_tool(name, args)
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
return await self.manager.execute_tool_for_sub_agent(name, args)
|
|
||||||
|
|
||||||
async def _execute_multi_agent_tool(self, name: str, args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
async def _execute_multi_agent_tool(self, name: str, args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
"""处理多智能体模式专属的通信工具。返回 None 表示不 属于多智能体工具。"""
|
"""处理多智能体模式专属的通信工具。返回 None 表示不 属于多智能体工具。"""
|
||||||
@ -768,8 +715,6 @@ class SubAgentTask:
|
|||||||
self.stats["write_files"] += 1
|
self.stats["write_files"] += 1
|
||||||
elif name == "edit_file":
|
elif name == "edit_file":
|
||||||
self.stats["edit_files"] += 1
|
self.stats["edit_files"] += 1
|
||||||
elif name == "search_workspace":
|
|
||||||
self.stats["searches"] += 1
|
|
||||||
elif name in ("web_search", "extract_webpage", "save_webpage"):
|
elif name in ("web_search", "extract_webpage", "save_webpage"):
|
||||||
self.stats["web_pages"] += 1
|
self.stats["web_pages"] += 1
|
||||||
elif name == "run_command":
|
elif name == "run_command":
|
||||||
@ -1016,21 +961,7 @@ class SubAgentTask:
|
|||||||
"stats": {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)},
|
"stats": {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)},
|
||||||
}
|
}
|
||||||
self.conversation_file.parent.mkdir(parents=True, exist_ok=True)
|
self.conversation_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_persist_before_dumps",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
messages_size=estimate_messages_size(self.messages),
|
|
||||||
)
|
|
||||||
conversation_json = json.dumps(conversation_data, ensure_ascii=False)
|
conversation_json = json.dumps(conversation_data, ensure_ascii=False)
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_persist_after_conversation_dumps",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
conversation_json_chars=len(conversation_json),
|
|
||||||
)
|
|
||||||
self.conversation_file.write_text(conversation_json, encoding="utf-8")
|
self.conversation_file.write_text(conversation_json, encoding="utf-8")
|
||||||
|
|
||||||
output_data = {
|
output_data = {
|
||||||
@ -1042,13 +973,6 @@ class SubAgentTask:
|
|||||||
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
output_json = json.dumps(output_data, ensure_ascii=False)
|
output_json = json.dumps(output_data, ensure_ascii=False)
|
||||||
self.output_file.write_text(output_json, encoding="utf-8")
|
self.output_file.write_text(output_json, encoding="utf-8")
|
||||||
log_memory_event(
|
|
||||||
"sub_agent_persist_after_output_dumps",
|
|
||||||
task_id=self.task_id,
|
|
||||||
agent_id=self.agent_id,
|
|
||||||
display_name=self.display_name,
|
|
||||||
output_json_chars=len(output_json),
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"[SubAgentTask] 增量保存失败: {exc}")
|
logger.warning(f"[SubAgentTask] 增量保存失败: {exc}")
|
||||||
|
|
||||||
|
|||||||
@ -145,29 +145,6 @@ SUB_AGENT_TOOLS: List[Dict[str, Any]] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "search_workspace",
|
|
||||||
"description": "在本地目录内搜索文件名或文件内容(跨文件)。仅返回摘要。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"mode": {"type": "string", "enum": ["file", "content"]},
|
|
||||||
"query": {"type": "string"},
|
|
||||||
"root": {"type": "string", "description": "搜索起点目录,默认 '.'。"},
|
|
||||||
"use_regex": {"type": "boolean"},
|
|
||||||
"case_sensitive": {"type": "boolean"},
|
|
||||||
"max_results": {"type": "integer"},
|
|
||||||
"max_matches_per_file": {"type": "integer"},
|
|
||||||
"include_glob": {"type": "array", "items": {"type": "string"}},
|
|
||||||
"exclude_glob": {"type": "array", "items": {"type": "string"}},
|
|
||||||
"max_file_size": {"type": "integer"},
|
|
||||||
},
|
|
||||||
"required": ["mode", "query"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@ -338,21 +315,6 @@ def _format_tool_result(name: str, raw: Any) -> str:
|
|||||||
if raw.get("mode") == "save":
|
if raw.get("mode") == "save":
|
||||||
return f"已保存: {raw.get('path')}"
|
return f"已保存: {raw.get('path')}"
|
||||||
return f"URL: {raw.get('url')}\n{raw.get('content') or ''}"
|
return f"URL: {raw.get('url')}\n{raw.get('content') or ''}"
|
||||||
if name == "search_workspace":
|
|
||||||
if raw.get("mode") == "file":
|
|
||||||
lines = [f"命中文件({len(raw.get('matches') or [])}):"]
|
|
||||||
for idx, p in enumerate(raw.get("matches") or [], 1):
|
|
||||||
lines.append(f"{idx}) {p}")
|
|
||||||
return "\n".join(lines)
|
|
||||||
if raw.get("mode") == "content":
|
|
||||||
parts = []
|
|
||||||
for item in raw.get("results") or []:
|
|
||||||
parts.append(item.get("file") or "")
|
|
||||||
for m in item.get("matches") or []:
|
|
||||||
parts.append(f"- L{m.get('line')}: {m.get('snippet')}")
|
|
||||||
parts.append("")
|
|
||||||
return "\n".join(parts).strip()
|
|
||||||
return ""
|
|
||||||
if name == "read_mediafile":
|
if name == "read_mediafile":
|
||||||
return raw.get("message") or "媒体已读取"
|
return raw.get("message") or "媒体已读取"
|
||||||
if name == "read_skill":
|
if name == "read_skill":
|
||||||
|
|||||||
@ -1,231 +1,16 @@
|
|||||||
"""子智能体特有工具实现(search_workspace / read_mediafile)。
|
"""子智能体特有工具实现(read_mediafile)。
|
||||||
|
|
||||||
这些工具在主进程中没有同名工具,因此由子智能体管理器本地实现。
|
|
||||||
search_workspace 优先复用主进程的 run_command 调用 rg,回退到 Python 遍历;
|
|
||||||
read_mediafile 直接读取项目内媒体文件并返回 base64。
|
read_mediafile 直接读取项目内媒体文件并返回 base64。
|
||||||
|
(原 search_workspace 工具已移除:子智能体需要搜索时改用 run_command
|
||||||
|
执行 rg / grep / find,走主进程沙箱链路,避免进程内遍历大目录导致内存失控。)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import fnmatch
|
|
||||||
import json
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
async def handle_search_workspace(
|
|
||||||
project_path: Path,
|
|
||||||
terminal: Any,
|
|
||||||
arguments: Dict[str, Any],
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""search_workspace 实现。"""
|
|
||||||
mode = arguments.get("mode")
|
|
||||||
query = arguments.get("query") or ""
|
|
||||||
root = arguments.get("root") or "."
|
|
||||||
use_regex = bool(arguments.get("use_regex", False))
|
|
||||||
case_sensitive = bool(arguments.get("case_sensitive", False))
|
|
||||||
max_results = int(arguments.get("max_results", 20))
|
|
||||||
max_matches_per_file = int(arguments.get("max_matches_per_file", 3))
|
|
||||||
include_glob = arguments.get("include_glob") or ["**/*"]
|
|
||||||
exclude_glob = arguments.get("exclude_glob") or []
|
|
||||||
max_file_size = arguments.get("max_file_size")
|
|
||||||
|
|
||||||
if mode not in {"file", "content"}:
|
|
||||||
return {"success": False, "error": "search_workspace mode 必须是 file 或 content"}
|
|
||||||
|
|
||||||
root_path = (project_path / root).resolve()
|
|
||||||
try:
|
|
||||||
root_path.relative_to(project_path)
|
|
||||||
except Exception:
|
|
||||||
return {"success": False, "error": "搜索根目录必须位于项目目录内"}
|
|
||||||
|
|
||||||
if mode == "file":
|
|
||||||
return _search_workspace_file(project_path, root_path, query, use_regex, case_sensitive, max_results, include_glob, exclude_glob)
|
|
||||||
|
|
||||||
rg_result = await _search_workspace_content_rg(
|
|
||||||
project_path, root_path, query, use_regex, case_sensitive, max_results, max_matches_per_file,
|
|
||||||
include_glob, exclude_glob, max_file_size, terminal
|
|
||||||
)
|
|
||||||
if rg_result is not None:
|
|
||||||
return rg_result
|
|
||||||
|
|
||||||
return _search_workspace_content_python(
|
|
||||||
project_path, root_path, query, use_regex, case_sensitive, max_results, max_matches_per_file,
|
|
||||||
include_glob, exclude_glob, max_file_size
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _search_workspace_file(
|
|
||||||
project_path: Path,
|
|
||||||
root_path: Path,
|
|
||||||
query: str,
|
|
||||||
use_regex: bool,
|
|
||||||
case_sensitive: bool,
|
|
||||||
max_results: int,
|
|
||||||
include_glob: List[str],
|
|
||||||
exclude_glob: List[str],
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
try:
|
|
||||||
pattern = re.compile(query, 0 if case_sensitive else re.IGNORECASE) if use_regex else None
|
|
||||||
needle = query if case_sensitive else query.lower()
|
|
||||||
matches = []
|
|
||||||
for item in _iter_files(project_path, root_path, include_glob, exclude_glob):
|
|
||||||
name = item.name
|
|
||||||
hay = name if case_sensitive else name.lower()
|
|
||||||
ok = pattern.search(name) if pattern else needle in hay
|
|
||||||
if ok:
|
|
||||||
matches.append(str(item.relative_to(project_path)))
|
|
||||||
if len(matches) >= max_results:
|
|
||||||
break
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"mode": "file",
|
|
||||||
"root": str(root_path.relative_to(project_path)),
|
|
||||||
"query": query,
|
|
||||||
"matches": matches,
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
|
||||||
return {"success": False, "error": str(exc)}
|
|
||||||
|
|
||||||
|
|
||||||
async def _search_workspace_content_rg(
|
|
||||||
project_path: Path,
|
|
||||||
root_path: Path,
|
|
||||||
query: str,
|
|
||||||
use_regex: bool,
|
|
||||||
case_sensitive: bool,
|
|
||||||
max_results: int,
|
|
||||||
max_matches_per_file: int,
|
|
||||||
include_glob: List[str],
|
|
||||||
exclude_glob: List[str],
|
|
||||||
max_file_size: Optional[int],
|
|
||||||
terminal: Any,
|
|
||||||
) -> Optional[Dict[str, Any]]:
|
|
||||||
if not shutil.which("rg"):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
args = ["rg", "--line-number", "--no-heading", "--color=never"]
|
|
||||||
if not use_regex:
|
|
||||||
args.append("--fixed-strings")
|
|
||||||
if not case_sensitive:
|
|
||||||
args.append("-i")
|
|
||||||
if max_matches_per_file:
|
|
||||||
args.append(f"--max-count={max_matches_per_file}")
|
|
||||||
if max_file_size:
|
|
||||||
args.append(f"--max-filesize={max_file_size}")
|
|
||||||
for g in include_glob:
|
|
||||||
args.extend(["-g", g])
|
|
||||||
for g in exclude_glob:
|
|
||||||
args.extend(["-g", f"!{g}"])
|
|
||||||
args.extend([query, str(root_path)])
|
|
||||||
|
|
||||||
result_text = await terminal.handle_tool_call("run_command", {"command": shlex.join(args), "timeout": 60})
|
|
||||||
parsed = json.loads(result_text) if isinstance(result_text, str) else result_text
|
|
||||||
if not parsed.get("success"):
|
|
||||||
return None
|
|
||||||
|
|
||||||
files_map: Dict[str, List[Dict[str, Any]]] = {}
|
|
||||||
for line in (parsed.get("output") or "").splitlines():
|
|
||||||
if not line.strip():
|
|
||||||
continue
|
|
||||||
parts = line.split(":", 2)
|
|
||||||
if len(parts) < 3:
|
|
||||||
continue
|
|
||||||
file_path, line_num, snippet = parts[0], parts[1], parts[2]
|
|
||||||
try:
|
|
||||||
line_no = int(line_num)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
files_map.setdefault(file_path, [])
|
|
||||||
if len(files_map[file_path]) < max_matches_per_file:
|
|
||||||
files_map[file_path].append({"line": line_no, "snippet": snippet.strip()})
|
|
||||||
if len(files_map) >= max_results and len(files_map[file_path]) >= max_matches_per_file:
|
|
||||||
break
|
|
||||||
|
|
||||||
results = [{"file": f, "matches": ms} for f, ms in list(files_map.items())[:max_results]]
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"mode": "content",
|
|
||||||
"root": str(root_path.relative_to(project_path)),
|
|
||||||
"query": query,
|
|
||||||
"results": results,
|
|
||||||
}
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _search_workspace_content_python(
|
|
||||||
project_path: Path,
|
|
||||||
root_path: Path,
|
|
||||||
query: str,
|
|
||||||
use_regex: bool,
|
|
||||||
case_sensitive: bool,
|
|
||||||
max_results: int,
|
|
||||||
max_matches_per_file: int,
|
|
||||||
include_glob: List[str],
|
|
||||||
exclude_glob: List[str],
|
|
||||||
max_file_size: Optional[int],
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
try:
|
|
||||||
pattern = re.compile(query, 0 if case_sensitive else re.IGNORECASE) if use_regex else None
|
|
||||||
needle = query if case_sensitive else query.lower()
|
|
||||||
results = []
|
|
||||||
for file_path in _iter_files(project_path, root_path, include_glob, exclude_glob):
|
|
||||||
if max_file_size:
|
|
||||||
try:
|
|
||||||
if file_path.stat().st_size > max_file_size:
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
lines = text.splitlines()
|
|
||||||
matches = []
|
|
||||||
for idx, line in enumerate(lines, 1):
|
|
||||||
hay = line if case_sensitive else line.lower()
|
|
||||||
ok = pattern.search(line) if pattern else needle in hay
|
|
||||||
if ok:
|
|
||||||
matches.append({"line": idx, "snippet": line.strip()})
|
|
||||||
if len(matches) >= max_matches_per_file:
|
|
||||||
break
|
|
||||||
if matches:
|
|
||||||
results.append({"file": str(file_path.relative_to(project_path)), "matches": matches})
|
|
||||||
if len(results) >= max_results:
|
|
||||||
break
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"mode": "content",
|
|
||||||
"root": str(root_path.relative_to(project_path)),
|
|
||||||
"query": query,
|
|
||||||
"results": results,
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
|
||||||
return {"success": False, "error": str(exc)}
|
|
||||||
|
|
||||||
|
|
||||||
def _iter_files(project_path: Path, root_path: Path, include_glob: List[str], exclude_glob: List[str]):
|
|
||||||
"""按 glob 规则遍历文件。"""
|
|
||||||
for item in root_path.rglob("*"):
|
|
||||||
if not item.is_file():
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
rel = item.relative_to(project_path)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
rel_str = str(rel)
|
|
||||||
included = any(fnmatch.fnmatch(rel_str, g) for g in include_glob)
|
|
||||||
if not included:
|
|
||||||
continue
|
|
||||||
if any(fnmatch.fnmatch(rel_str, g) for g in exclude_glob):
|
|
||||||
continue
|
|
||||||
yield item
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_read_mediafile(project_path: Path, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
async def handle_read_mediafile(project_path: Path, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
|||||||
@ -54,8 +54,7 @@
|
|||||||
你拥有以下工具能力:
|
你拥有以下工具能力:
|
||||||
- read_file: 读取文件内容
|
- read_file: 读取文件内容
|
||||||
- write_file / edit_file: 创建或修改文件
|
- write_file / edit_file: 创建或修改文件
|
||||||
- search_workspace: 搜索文件和代码
|
- run_command: 执行终端命令(需要搜索文件/代码时,用它执行 rg 或 grep -rn / find;务必排除 node_modules、.git 等重目录,例如 grep -rn --exclude-dir=node_modules --exclude-dir=.git)
|
||||||
- run_command: 执行终端命令
|
|
||||||
- web_search / extract_webpage: 搜索和提取网页内容
|
- web_search / extract_webpage: 搜索和提取网页内容
|
||||||
- read_mediafile: 读取图片/视频文件
|
- read_mediafile: 读取图片/视频文件
|
||||||
- finish_task: 完成任务并退出(必须调用)
|
- finish_task: 完成任务并退出(必须调用)
|
||||||
|
|||||||
@ -46,7 +46,6 @@ from modules.user_manager import UserWorkspace
|
|||||||
from modules.usage_tracker import QUOTA_DEFAULTS
|
from modules.usage_tracker import QUOTA_DEFAULTS
|
||||||
from modules.sub_agent import TERMINAL_STATUSES
|
from modules.sub_agent import TERMINAL_STATUSES
|
||||||
from modules.multi_agent.debug_logger import ma_debug
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
from modules.memory_debug import log_memory_event, estimate_messages_size
|
|
||||||
from modules.versioning_manager import ConversationVersioningManager, VersioningError
|
from modules.versioning_manager import ConversationVersioningManager, VersioningError
|
||||||
from modules.shallow_versioning import ShallowVersioningManager
|
from modules.shallow_versioning import ShallowVersioningManager
|
||||||
from core.web_terminal import WebTerminal
|
from core.web_terminal import WebTerminal
|
||||||
@ -1362,12 +1361,6 @@ async def handle_task_with_sender(
|
|||||||
message_preview=str(message)[:300] if message else None,
|
message_preview=str(message)[:300] if message else None,
|
||||||
pending_master_messages_count=pending_count,
|
pending_master_messages_count=pending_count,
|
||||||
)
|
)
|
||||||
log_memory_event(
|
|
||||||
"handle_task_with_sender_start",
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
multi_agent_mode=True,
|
|
||||||
pending_master_messages_count=pending_count,
|
|
||||||
)
|
|
||||||
videos = videos or []
|
videos = videos or []
|
||||||
raw_sender = sender
|
raw_sender = sender
|
||||||
|
|
||||||
@ -2508,14 +2501,6 @@ async def handle_task_with_sender(
|
|||||||
finalize_run_versioning_checkpoint("completed")
|
finalize_run_versioning_checkpoint("completed")
|
||||||
else:
|
else:
|
||||||
finalize_run_versioning_checkpoint("waiting_background")
|
finalize_run_versioning_checkpoint("waiting_background")
|
||||||
log_memory_event(
|
|
||||||
"handle_task_with_sender_complete",
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
multi_agent_mode=bool(getattr(web_terminal, "multi_agent_mode", False)),
|
|
||||||
total_iterations=total_iterations,
|
|
||||||
total_tool_calls=total_tool_calls,
|
|
||||||
has_running_multi_agent=has_running_multi_agent or has_pending_ma_messages,
|
|
||||||
)
|
|
||||||
sender('task_complete', {
|
sender('task_complete', {
|
||||||
'total_iterations': total_iterations,
|
'total_iterations': total_iterations,
|
||||||
'total_tool_calls': total_tool_calls,
|
'total_tool_calls': total_tool_calls,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ from typing import Any, Callable, Dict, List, Optional
|
|||||||
|
|
||||||
from modules.sub_agent import TERMINAL_STATUSES
|
from modules.sub_agent import TERMINAL_STATUSES
|
||||||
from modules.multi_agent.debug_logger import ma_debug
|
from modules.multi_agent.debug_logger import ma_debug
|
||||||
from modules.memory_debug import log_memory_event, estimate_messages_size
|
|
||||||
|
|
||||||
|
|
||||||
_VALID_SOURCES = {
|
_VALID_SOURCES = {
|
||||||
@ -351,14 +350,6 @@ def inject_multi_agent_master_message(
|
|||||||
inline=inline,
|
inline=inline,
|
||||||
after_tool_call_id=after_tool_call_id,
|
after_tool_call_id=after_tool_call_id,
|
||||||
)
|
)
|
||||||
log_memory_event(
|
|
||||||
"inject_multi_agent_master_message",
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
inline=inline,
|
|
||||||
text_len=len(raw),
|
|
||||||
messages_size=estimate_messages_size(messages),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 解析标准格式以获取 subtype;解析失败时回退到通用类型
|
# 解析标准格式以获取 subtype;解析失败时回退到通用类型
|
||||||
try:
|
try:
|
||||||
from modules.multi_agent.state import parse_multi_agent_message
|
from modules.multi_agent.state import parse_multi_agent_message
|
||||||
@ -475,13 +466,6 @@ async def process_multi_agent_master_messages(
|
|||||||
return 0
|
return 0
|
||||||
ma_debug("process_ma_messages_got_state", conversation_id=conversation_id, state_id=id(state))
|
ma_debug("process_ma_messages_got_state", conversation_id=conversation_id, state_id=id(state))
|
||||||
pending = state.drain_master_messages()
|
pending = state.drain_master_messages()
|
||||||
log_memory_event(
|
|
||||||
"process_multi_agent_master_messages",
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
inline=inline,
|
|
||||||
pending_count=len(pending),
|
|
||||||
messages_size=estimate_messages_size(messages),
|
|
||||||
)
|
|
||||||
if not pending:
|
if not pending:
|
||||||
ma_debug("process_ma_messages_empty", conversation_id=conversation_id, state_id=id(state))
|
ma_debug("process_ma_messages_empty", conversation_id=conversation_id, state_id=id(state))
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@ -6,9 +6,6 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from modules.memory_debug import log_memory_event, estimate_messages_size
|
|
||||||
|
|
||||||
|
|
||||||
def _load_summary_prompt(web_terminal) -> str:
|
def _load_summary_prompt(web_terminal) -> str:
|
||||||
"""从 prompts/deep_compression_summary.txt 加载压缩总结提示词。"""
|
"""从 prompts/deep_compression_summary.txt 加载压缩总结提示词。"""
|
||||||
try:
|
try:
|
||||||
@ -313,11 +310,6 @@ async def run_deep_compression(
|
|||||||
mode: str,
|
mode: str,
|
||||||
sender=None,
|
sender=None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
log_memory_event(
|
|
||||||
"run_deep_compression_enter",
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
mode=mode,
|
|
||||||
)
|
|
||||||
cm = web_terminal.context_manager
|
cm = web_terminal.context_manager
|
||||||
target_manager = (
|
target_manager = (
|
||||||
cm._get_conversation_manager_for_id(conversation_id)
|
cm._get_conversation_manager_for_id(conversation_id)
|
||||||
@ -516,14 +508,6 @@ async def run_deep_compression(
|
|||||||
"compress_behavior": compress_behavior,
|
"compress_behavior": compress_behavior,
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
})
|
})
|
||||||
log_memory_event(
|
|
||||||
"run_deep_compression_exit",
|
|
||||||
conversation_id=conversation_id,
|
|
||||||
mode=mode,
|
|
||||||
marked_count=marked_count,
|
|
||||||
history_size=estimate_messages_size(cm.conversation_history),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"in_place": True,
|
"in_place": True,
|
||||||
|
|||||||
@ -104,10 +104,6 @@ const buildText = (entry: ActivityEntry) => {
|
|||||||
const skillName = args.skill_name || '';
|
const skillName = args.skill_name || '';
|
||||||
return `阅读技能 ${skillName}`;
|
return `阅读技能 ${skillName}`;
|
||||||
}
|
}
|
||||||
if (tool === 'search_workspace') {
|
|
||||||
const query = args.query || args.keyword || '';
|
|
||||||
return `在工作区搜索 ${query}`;
|
|
||||||
}
|
|
||||||
if (tool === 'web_search') {
|
if (tool === 'web_search') {
|
||||||
const query = args.query || args.q || '';
|
const query = args.query || args.q || '';
|
||||||
return `在互联网中搜索 ${query}`;
|
return `在互联网中搜索 ${query}`;
|
||||||
|
|||||||
@ -185,63 +185,6 @@ def test_write_file_and_edit():
|
|||||||
shutil.rmtree(tmp, ignore_errors=True)
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def test_search_workspace():
|
|
||||||
"""测试 search_workspace 走 run_command rg 回退。"""
|
|
||||||
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
|
|
||||||
try:
|
|
||||||
manager = SubAgentManager(str(tmp), str(tmp / "data"))
|
|
||||||
terminal = FakeTerminal(tmp)
|
|
||||||
manager.set_terminal(terminal)
|
|
||||||
|
|
||||||
(tmp / "findme.txt").write_text("hello world unique string", encoding="utf-8")
|
|
||||||
|
|
||||||
async def fake_search(project_path, terminal_, arguments):
|
|
||||||
query = arguments["query"]
|
|
||||||
if arguments.get("mode") == "file":
|
|
||||||
matches = [p.name for p in project_path.rglob("*") if p.is_file() and query in p.name]
|
|
||||||
return {"success": True, "mode": "file", "matches": matches}
|
|
||||||
matches = []
|
|
||||||
for p in project_path.rglob("*.txt"):
|
|
||||||
text = p.read_text(encoding="utf-8")
|
|
||||||
if query in text:
|
|
||||||
matches.append({"file": str(p.relative_to(project_path)), "matches": [{"line": 1, "snippet": text}]})
|
|
||||||
return {"success": True, "mode": "content", "results": matches}
|
|
||||||
|
|
||||||
import modules.sub_agent.tools as sat
|
|
||||||
original_search = sat.handle_search_workspace
|
|
||||||
sat.handle_search_workspace = fake_search
|
|
||||||
|
|
||||||
responses = [
|
|
||||||
[make_tool_chunk([{
|
|
||||||
"name": "search_workspace",
|
|
||||||
"args": json.dumps({"mode": "content", "query": "unique string"})
|
|
||||||
}])],
|
|
||||||
[make_tool_chunk([{
|
|
||||||
"name": "finish_task",
|
|
||||||
"args": json.dumps({"success": True, "summary": "搜索完成"})
|
|
||||||
}])],
|
|
||||||
]
|
|
||||||
_, original_model = install_fake_model(responses)
|
|
||||||
try:
|
|
||||||
result = manager.create_sub_agent(
|
|
||||||
agent_id=2,
|
|
||||||
summary="搜索测试",
|
|
||||||
task="测试 search_workspace",
|
|
||||||
deliverables_dir="deliverables2",
|
|
||||||
timeout_seconds=30,
|
|
||||||
conversation_id="conv-test-2",
|
|
||||||
thinking_mode="fast",
|
|
||||||
)
|
|
||||||
assert result["success"], result
|
|
||||||
final = manager.wait_for_completion(task_id=result["task_id"], timeout_seconds=10)
|
|
||||||
assert final["status"] == "completed", final
|
|
||||||
finally:
|
|
||||||
restore_model(original_model)
|
|
||||||
sat.handle_search_workspace = original_search
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(tmp, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_mediafile():
|
def test_read_mediafile():
|
||||||
"""测试 read_mediafile 返回 base64。"""
|
"""测试 read_mediafile 返回 base64。"""
|
||||||
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
|
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
|
||||||
@ -341,7 +284,6 @@ def run_all():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
tests = [
|
tests = [
|
||||||
("write_file + edit_file", test_write_file_and_edit),
|
("write_file + edit_file", test_write_file_and_edit),
|
||||||
("search_workspace", test_search_workspace),
|
|
||||||
("read_mediafile", test_read_mediafile),
|
("read_mediafile", test_read_mediafile),
|
||||||
("terminate", test_terminate),
|
("terminate", test_terminate),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -35,7 +35,6 @@ from utils.log_rotation import append_line, prune_dir
|
|||||||
|
|
||||||
|
|
||||||
from utils.api_client.utils import _api_dump_enabled
|
from utils.api_client.utils import _api_dump_enabled
|
||||||
from modules.memory_debug import log_memory_event, estimate_messages_size
|
|
||||||
|
|
||||||
class DeepSeekClientChatMixin:
|
class DeepSeekClientChatMixin:
|
||||||
async def chat(
|
async def chat(
|
||||||
@ -101,13 +100,6 @@ class DeepSeekClientChatMixin:
|
|||||||
final_messages = self._merge_system_messages(messages)
|
final_messages = self._merge_system_messages(messages)
|
||||||
final_messages = self._sanitize_messages_for_model_capability(final_messages)
|
final_messages = self._sanitize_messages_for_model_capability(final_messages)
|
||||||
final_messages = self._sanitize_message_fields_for_api(final_messages)
|
final_messages = self._sanitize_message_fields_for_api(final_messages)
|
||||||
log_memory_event(
|
|
||||||
"api_client_chat_request_prepared",
|
|
||||||
model_key=self.model_key,
|
|
||||||
input_messages_size=estimate_messages_size(messages),
|
|
||||||
final_messages_size=estimate_messages_size(final_messages),
|
|
||||||
)
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": api_config["model_id"],
|
"model": api_config["model_id"],
|
||||||
"messages": final_messages,
|
"messages": final_messages,
|
||||||
@ -143,11 +135,6 @@ class DeepSeekClientChatMixin:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
dump_path = self._dump_request_payload(payload, api_config, headers)
|
dump_path = self._dump_request_payload(payload, api_config, headers)
|
||||||
log_memory_event(
|
|
||||||
"api_client_chat_request_payload",
|
|
||||||
model_key=self.model_key,
|
|
||||||
payload_chars=len(json.dumps(payload, ensure_ascii=False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(http2=True, timeout=300) as client:
|
async with httpx.AsyncClient(http2=True, timeout=300) as client:
|
||||||
@ -197,15 +184,7 @@ class DeepSeekClientChatMixin:
|
|||||||
yield {"error": self.last_error_info}
|
yield {"error": self.last_error_info}
|
||||||
return
|
return
|
||||||
|
|
||||||
chunk_count = 0
|
|
||||||
async for line in response.aiter_lines():
|
async for line in response.aiter_lines():
|
||||||
chunk_count += 1
|
|
||||||
if chunk_count % 100 == 0:
|
|
||||||
log_memory_event(
|
|
||||||
"api_client_stream_chunk",
|
|
||||||
model_key=self.model_key,
|
|
||||||
chunk_count=chunk_count,
|
|
||||||
)
|
|
||||||
if line.startswith("data:"):
|
if line.startswith("data:"):
|
||||||
json_str = line[5:].strip()
|
json_str = line[5:].strip()
|
||||||
if json_str == "[DONE]":
|
if json_str == "[DONE]":
|
||||||
@ -222,16 +201,6 @@ class DeepSeekClientChatMixin:
|
|||||||
json=payload,
|
json=payload,
|
||||||
headers=headers
|
headers=headers
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
response_text = response.text
|
|
||||||
except Exception:
|
|
||||||
response_text = ""
|
|
||||||
log_memory_event(
|
|
||||||
"api_client_nonstream_response",
|
|
||||||
model_key=self.model_key,
|
|
||||||
status_code=response.status_code,
|
|
||||||
response_chars=len(response_text),
|
|
||||||
)
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
error_text = response.text
|
error_text = response.text
|
||||||
self.last_error_info = {
|
self.last_error_info = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user