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.tool_result_formatter import format_tool_result_for_context
|
||||
from utils.logger import setup_logger
|
||||
from modules.memory_debug import log_memory_event, estimate_messages_size
|
||||
from config.model_profiles import (
|
||||
get_model_profile,
|
||||
get_model_prompt_replacements,
|
||||
@ -98,12 +97,6 @@ class MessagesMixin:
|
||||
|
||||
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:
|
||||
file_tree_preview = (context.get("project_info", {}).get("file_tree") or "").splitlines()
|
||||
write_host_workspace_debug(
|
||||
@ -423,12 +416,4 @@ class MessagesMixin:
|
||||
print(f"[ContextCompression] build_messages 替换tool占位符: {replaced_tool_count} 条")
|
||||
if 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
|
||||
|
||||
@ -20,7 +20,6 @@ except ImportError:
|
||||
sys.path.insert(0, str(project_root))
|
||||
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
|
||||
from modules.terminal_manager import TerminalManager
|
||||
from modules.memory_debug import start_periodic_sampling
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from modules.user_container_manager import ContainerHandle
|
||||
@ -102,9 +101,6 @@ class WebTerminal(MainTerminal):
|
||||
print(f"[WebTerminal] 实时token统计已启用")
|
||||
else:
|
||||
print(f"[WebTerminal] 警告:message_callback为None,无法启用实时token统计")
|
||||
|
||||
# 启动后台内存采样,帮助定位偶发内存暴涨
|
||||
start_periodic_sampling()
|
||||
# ===========================================
|
||||
# 新增:对话管理相关方法(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 modules.sub_agent.task import SubAgentTask
|
||||
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.stats import SubAgentStatsMixin
|
||||
from modules.sub_agent.creation import SubAgentCreationMixin
|
||||
from modules.multi_agent.debug_logger import ma_debug
|
||||
from modules.memory_debug import log_memory_event
|
||||
from server.utils_common import debug_log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -285,16 +284,6 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
conversation_id=conversation_id,
|
||||
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()
|
||||
asyncio_task = self._run_coro(task_coro)
|
||||
sub_agent._task = asyncio_task
|
||||
@ -304,13 +293,6 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
|
||||
def _on_done(fut):
|
||||
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)
|
||||
# 多智能体模式下 failed 视为可复活状态,保留实例引用供后续 send_message_to_sub_agent 重新激活
|
||||
if multi_agent_mode:
|
||||
@ -711,18 +693,11 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
try:
|
||||
# 多智能体模式常见问答工具已在 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":
|
||||
return await handle_read_mediafile(self.project_path, arguments)
|
||||
|
||||
# 其余工具直接走主进程 handle_tool_call,自然经过沙箱/容器/权限链路
|
||||
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:
|
||||
return json.loads(result_text)
|
||||
except Exception:
|
||||
@ -766,27 +741,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
continue
|
||||
|
||||
try:
|
||||
log_memory_event(
|
||||
"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)
|
||||
conversation_data = json.loads(conversation_file.read_text(encoding="utf-8"))
|
||||
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:
|
||||
logger.warning(f"[restore] 读取任务 {task_id} 对话文件失败: {exc}")
|
||||
continue
|
||||
|
||||
@ -28,7 +28,6 @@ if TYPE_CHECKING:
|
||||
from modules.multi_agent.state import MultiAgentState
|
||||
|
||||
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__)
|
||||
|
||||
@ -121,15 +120,6 @@ class SubAgentTask:
|
||||
self._pending_answer_question_id: Optional[str] = None
|
||||
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:
|
||||
"""输出一行 JSONL 到 progress 文件并缓存。"""
|
||||
line = json.dumps({"type": type_, **data}, ensure_ascii=False)
|
||||
@ -273,30 +263,13 @@ class SubAgentTask:
|
||||
pending_answer_question_id=self._pending_answer_question_id,
|
||||
)
|
||||
|
||||
async with async_mem_snapshot(
|
||||
"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)
|
||||
assistant_message, reasoning, tool_calls, usage = await self._call_model(client, model_key, tools)
|
||||
if usage:
|
||||
self._apply_usage(usage)
|
||||
|
||||
# 上下文压缩检查:超过阈值时触发深度压缩
|
||||
if self.current_context_tokens > 0 and self.current_context_tokens >= self.compress_threshold_tokens:
|
||||
with mem_snapshot(
|
||||
"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()
|
||||
compressed = self._deep_compress_messages()
|
||||
if compressed:
|
||||
ma_debug(
|
||||
"sub_agent_deep_compressed",
|
||||
@ -414,15 +387,6 @@ class SubAgentTask:
|
||||
output_len=len(output_text),
|
||||
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:
|
||||
ma_debug("sub_agent_forward_no_state", task_id=self.task_id, agent_id=self.agent_id)
|
||||
return
|
||||
@ -638,15 +602,6 @@ class SubAgentTask:
|
||||
if chunk.get("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
|
||||
|
||||
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/
|
||||
list_active_sub_agents)在主进程内直接处理,不再转发到 WebTerminal。
|
||||
"""
|
||||
async with async_mem_snapshot(
|
||||
"sub_agent_execute_tool",
|
||||
task_id=self.task_id,
|
||||
agent_id=self.agent_id,
|
||||
display_name=self.display_name,
|
||||
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)
|
||||
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]]:
|
||||
"""处理多智能体模式专属的通信工具。返回 None 表示不 属于多智能体工具。"""
|
||||
@ -768,8 +715,6 @@ class SubAgentTask:
|
||||
self.stats["write_files"] += 1
|
||||
elif name == "edit_file":
|
||||
self.stats["edit_files"] += 1
|
||||
elif name == "search_workspace":
|
||||
self.stats["searches"] += 1
|
||||
elif name in ("web_search", "extract_webpage", "save_webpage"):
|
||||
self.stats["web_pages"] += 1
|
||||
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)},
|
||||
}
|
||||
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)
|
||||
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")
|
||||
|
||||
output_data = {
|
||||
@ -1042,13 +973,6 @@ class SubAgentTask:
|
||||
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_json = json.dumps(output_data, ensure_ascii=False)
|
||||
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:
|
||||
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",
|
||||
"function": {
|
||||
@ -338,21 +315,6 @@ def _format_tool_result(name: str, raw: Any) -> str:
|
||||
if raw.get("mode") == "save":
|
||||
return f"已保存: {raw.get('path')}"
|
||||
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":
|
||||
return raw.get("message") or "媒体已读取"
|
||||
if name == "read_skill":
|
||||
|
||||
@ -1,231 +1,16 @@
|
||||
"""子智能体特有工具实现(search_workspace / read_mediafile)。
|
||||
"""子智能体特有工具实现(read_mediafile)。
|
||||
|
||||
这些工具在主进程中没有同名工具,因此由子智能体管理器本地实现。
|
||||
search_workspace 优先复用主进程的 run_command 调用 rg,回退到 Python 遍历;
|
||||
read_mediafile 直接读取项目内媒体文件并返回 base64。
|
||||
(原 search_workspace 工具已移除:子智能体需要搜索时改用 run_command
|
||||
执行 rg / grep / find,走主进程沙箱链路,避免进程内遍历大目录导致内存失控。)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import fnmatch
|
||||
import json
|
||||
import mimetypes
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
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
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
async def handle_read_mediafile(project_path: Path, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
@ -54,8 +54,7 @@
|
||||
你拥有以下工具能力:
|
||||
- read_file: 读取文件内容
|
||||
- write_file / edit_file: 创建或修改文件
|
||||
- search_workspace: 搜索文件和代码
|
||||
- run_command: 执行终端命令
|
||||
- run_command: 执行终端命令(需要搜索文件/代码时,用它执行 rg 或 grep -rn / find;务必排除 node_modules、.git 等重目录,例如 grep -rn --exclude-dir=node_modules --exclude-dir=.git)
|
||||
- web_search / extract_webpage: 搜索和提取网页内容
|
||||
- read_mediafile: 读取图片/视频文件
|
||||
- finish_task: 完成任务并退出(必须调用)
|
||||
|
||||
@ -46,7 +46,6 @@ from modules.user_manager import UserWorkspace
|
||||
from modules.usage_tracker import QUOTA_DEFAULTS
|
||||
from modules.sub_agent import TERMINAL_STATUSES
|
||||
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.shallow_versioning import ShallowVersioningManager
|
||||
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,
|
||||
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 []
|
||||
raw_sender = sender
|
||||
|
||||
@ -2508,14 +2501,6 @@ async def handle_task_with_sender(
|
||||
finalize_run_versioning_checkpoint("completed")
|
||||
else:
|
||||
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', {
|
||||
'total_iterations': total_iterations,
|
||||
'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.multi_agent.debug_logger import ma_debug
|
||||
from modules.memory_debug import log_memory_event, estimate_messages_size
|
||||
|
||||
|
||||
_VALID_SOURCES = {
|
||||
@ -351,14 +350,6 @@ def inject_multi_agent_master_message(
|
||||
inline=inline,
|
||||
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;解析失败时回退到通用类型
|
||||
try:
|
||||
from modules.multi_agent.state import parse_multi_agent_message
|
||||
@ -475,13 +466,6 @@ async def process_multi_agent_master_messages(
|
||||
return 0
|
||||
ma_debug("process_ma_messages_got_state", conversation_id=conversation_id, state_id=id(state))
|
||||
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:
|
||||
ma_debug("process_ma_messages_empty", conversation_id=conversation_id, state_id=id(state))
|
||||
return 0
|
||||
|
||||
@ -6,9 +6,6 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
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:
|
||||
"""从 prompts/deep_compression_summary.txt 加载压缩总结提示词。"""
|
||||
try:
|
||||
@ -313,11 +310,6 @@ async def run_deep_compression(
|
||||
mode: str,
|
||||
sender=None,
|
||||
) -> Dict[str, Any]:
|
||||
log_memory_event(
|
||||
"run_deep_compression_enter",
|
||||
conversation_id=conversation_id,
|
||||
mode=mode,
|
||||
)
|
||||
cm = web_terminal.context_manager
|
||||
target_manager = (
|
||||
cm._get_conversation_manager_for_id(conversation_id)
|
||||
@ -516,14 +508,6 @@ async def run_deep_compression(
|
||||
"compress_behavior": compress_behavior,
|
||||
"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 {
|
||||
"success": True,
|
||||
"in_place": True,
|
||||
|
||||
@ -104,10 +104,6 @@ const buildText = (entry: ActivityEntry) => {
|
||||
const skillName = args.skill_name || '';
|
||||
return `阅读技能 ${skillName}`;
|
||||
}
|
||||
if (tool === 'search_workspace') {
|
||||
const query = args.query || args.keyword || '';
|
||||
return `在工作区搜索 ${query}`;
|
||||
}
|
||||
if (tool === 'web_search') {
|
||||
const query = args.query || args.q || '';
|
||||
return `在互联网中搜索 ${query}`;
|
||||
|
||||
@ -185,63 +185,6 @@ def test_write_file_and_edit():
|
||||
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():
|
||||
"""测试 read_mediafile 返回 base64。"""
|
||||
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
|
||||
@ -341,7 +284,6 @@ def run_all():
|
||||
print("=" * 60)
|
||||
tests = [
|
||||
("write_file + edit_file", test_write_file_and_edit),
|
||||
("search_workspace", test_search_workspace),
|
||||
("read_mediafile", test_read_mediafile),
|
||||
("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 modules.memory_debug import log_memory_event, estimate_messages_size
|
||||
|
||||
class DeepSeekClientChatMixin:
|
||||
async def chat(
|
||||
@ -101,13 +100,6 @@ class DeepSeekClientChatMixin:
|
||||
final_messages = self._merge_system_messages(messages)
|
||||
final_messages = self._sanitize_messages_for_model_capability(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 = {
|
||||
"model": api_config["model_id"],
|
||||
"messages": final_messages,
|
||||
@ -143,11 +135,6 @@ class DeepSeekClientChatMixin:
|
||||
except Exception:
|
||||
pass
|
||||
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:
|
||||
async with httpx.AsyncClient(http2=True, timeout=300) as client:
|
||||
@ -197,15 +184,7 @@ class DeepSeekClientChatMixin:
|
||||
yield {"error": self.last_error_info}
|
||||
return
|
||||
|
||||
chunk_count = 0
|
||||
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:"):
|
||||
json_str = line[5:].strip()
|
||||
if json_str == "[DONE]":
|
||||
@ -222,16 +201,6 @@ class DeepSeekClientChatMixin:
|
||||
json=payload,
|
||||
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:
|
||||
error_text = response.text
|
||||
self.last_error_info = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user