agent-Specialization/core/main_terminal_parts/context.py
JOJO 9ed956518c refactor(sub_agent): 子智能体从 Node.js 子进程改为主进程内 Python 协程
- 重写子智能体执行核心,不再启动 easyagent Node.js 子进程
- 新增 modules/sub_agent/ 包集中管理子智能体逻辑
- 工具调用复用主进程 WebTerminal.handle_tool_call,自然经过沙箱/容器链路
- 子智能体模型独立读取 ~/.agents/<mode>/config/sub_agent_models.json
- 支持 8 个工具:read_file/write_file/edit_file(replacements+replace_all)/run_command/web_search/extract_webpage/search_workspace/read_mediafile
- 修复子智能体进度弹窗:标题颜色、write_file 显示、过滤非 progress 条目、统一滚动条样式
- 更新 AGENTS.md / CLAUDE.md 子智能体描述
- 新增 test/test_sub_agent_regression.py 回归测试
2026-06-20 00:26:45 +08:00

681 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
try:
from config import (
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
READ_TOOL_MAX_FILE_SIZE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
CUSTOM_TOOLS_ENABLED,
)
except ImportError:
import sys
project_root = Path(__file__).resolve().parents[2]
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from config import (
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
READ_TOOL_MAX_FILE_SIZE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
CUSTOM_TOOLS_ENABLED,
)
from modules.file_manager import FileManager
from modules.search_engine import SearchEngine
from modules.terminal_ops import TerminalOperator
from modules.memory_manager import MemoryManager
from modules.terminal_manager import TerminalManager
from modules.todo_manager import TodoManager
from modules.sub_agent import SubAgentManager
from modules.webpage_extractor import extract_webpage_content, tavily_extract
from modules.ocr_client import OCRClient
from modules.easter_egg_manager import EasterEggManager
from modules.personalization_manager import (
load_personalization_config,
build_personalization_prompt,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
RECENT_CONVERSATIONS_PROMPT_LIMIT_DEFAULT,
)
from modules.skills_manager import (
get_skills_catalog,
build_skills_list,
merge_enabled_skills,
build_skills_prompt,
infer_private_skills_dir,
)
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES
from utils.api_client import DeepSeekClient
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 config.model_profiles import (
get_model_profile,
get_model_prompt_replacements,
get_model_context_window,
model_supports_image,
model_supports_video,
)
logger = setup_logger(__name__)
DISABLE_LENGTH_CHECK = True
class MainTerminalContextMixin:
# 权限模式说明(代码中存储的简要版本)
_PERMISSION_MODE_DESC = {
"unrestricted": "当前处于无限制模式,所有工具均可直接使用,无需额外批准。",
"readonly": "当前处于只读模式,仅能执行不会修改工作区的读取类操作。",
"approval": "当前处于批准模式,修改工作区的操作需要用户批准后方可执行。",
"auto_approval": "当前处于自动审核模式,工作区内文件编辑可直接执行,高风险操作由后台审批智能体自动审核。",
}
_PERMISSION_MODE_LABEL = {
"unrestricted": "无限制",
"readonly": "只读",
"approval": "批准",
"auto_approval": "自动审核",
}
_EXECUTION_MODE_LABEL = {
"sandbox": "沙箱",
"direct": "完全访问权限",
}
def _build_permission_mode_message(self) -> Optional[str]:
"""根据当前权限模式构建权限说明消息(从模板读取并替换占位符)"""
template = self.load_prompt("permission_mode")
if not template:
return None
mode = self.get_permission_mode()
# 根据模式生成详细规则
if mode == "unrestricted":
detailed_rules = (
"- 所有命令和工具均可直接使用,无需用户批准\n"
"- run_command支持任意命令包括管道、重定向、子shell等\n"
"- 文件操作read_file / write_file / edit_file 可直接使用;其他文件管理请通过 run_command 执行;需要 Python 时先探测并选择合适解释器"
)
elif mode == "readonly":
detailed_rules = (
"- run_command统一在系统只读沙箱中执行\n"
"- 若命令触发写入,系统会直接拒绝并返回权限错误\n"
"- 文件操作:仅允许 read_file、view_image、view_video 等读取类工具\n"
"- 禁止write_file、edit_file 及会修改工作区的命令操作"
)
elif mode == "approval":
detailed_rules = (
"- run_command先在系统只读沙箱执行仅当出现权限拒绝时才触发审批\n"
"- 审批通过后:仅当前这一次命令会重试为可写沙箱执行\n"
"- 需要用户批准terminal_input、write_file、edit_file、save_webpage 及其他会修改工作区的操作\n"
"- 被拒绝或超时:本次操作不会执行写入"
)
elif mode == "auto_approval":
detailed_rules = (
"- write_file / edit_file若路径在当前工作区内直接执行工作区外路径会进入审批流程\n"
"- run_command先在系统只读沙箱执行仅当出现权限拒绝时才触发自动审批\n"
"- 自动审批由后台审批智能体执行,默认只判断危险性与越权风险,不判断任务必要性\n"
"- 自动审批拒绝:本次工具调用会返回“拒绝+理由”,主循环继续;人工拒绝可随时接管\n"
"- 可随时人工接管:用户在审批面板点击同意/拒绝/切换无限制后,自动审批会立即停止并以人工决策为准"
)
else:
detailed_rules = ""
return template.format(
permission_mode=mode,
permission_mode_label=self._PERMISSION_MODE_LABEL.get(mode, mode),
mode_description=self._PERMISSION_MODE_DESC.get(mode, ""),
detailed_rules=detailed_rules,
)
def _build_execution_mode_message(self) -> Optional[str]:
"""根据当前执行环境模式构建提示消息。"""
# 仅宿主机模式注入Docker 模式不需要该提示。
try:
if not getattr(self, "_is_host_mode", lambda: False)():
return None
except Exception:
return None
template = self.load_prompt("execution_mode")
if not template:
return None
state = {}
if hasattr(self, "get_execution_mode_state"):
try:
state = self.get_execution_mode_state() or {}
except Exception:
state = {}
mode = str(state.get("mode") or "sandbox").strip().lower()
mode_label = self._EXECUTION_MODE_LABEL.get(mode, mode)
if mode == "sandbox":
rules = (
"- 所有命令默认在系统 OS 沙箱中执行\n"
"- 若操作受系统权限限制:先提供 1 个安全替代方案并执行;若仍无法满足目标,明确请求用户切换执行环境为“完全访问权限”后重试\n"
"- 不要通过复杂绕过手段反复尝试规避沙箱限制;若任务本质需要更高权限,应直接说明并等待用户确认"
)
net = getattr(self, "host_network_permission", "restricted")
if net == "restricted":
network_rules = "- 网络:受限(仅允许 localhost外部网络不可达"
elif net == "full":
network_rules = "- 网络:完全开放"
else:
network_rules = ""
else:
rules = (
"- 当前为宿主机直接执行模式(完全访问权限)\n"
"- 仅在必须时执行高权限操作,保持最小化命令范围\n"
"- 涉及删除/覆盖/系统级变更前,先说明风险再执行"
)
network_rules = ""
return template.format(
execution_mode=mode,
execution_mode_label=mode_label,
rules=rules,
network_rules=network_rules,
)
def _get_or_init_frozen_mode_prompt(self, key: str, builder) -> Optional[str]:
return self._get_or_init_frozen_prompt(key, builder)
def _get_or_init_frozen_prompt(self, key: str, builder) -> Optional[str]:
cm = getattr(self, "context_manager", None)
meta = getattr(cm, "conversation_metadata", {}) if cm else {}
cached = meta.get(key) if isinstance(meta, dict) else None
if isinstance(cached, str) and cached:
return cached
built = builder() or ""
conv_id = getattr(cm, "current_conversation_id", None) if cm else None
if cm and conv_id:
try:
cm.conversation_manager.update_conversation_metadata(conv_id, {key: built})
if isinstance(cm.conversation_metadata, dict):
cm.conversation_metadata[key] = built
except Exception:
pass
return built
def build_context(self) -> Dict:
"""构建主终端上下文"""
# 读取记忆
memory = self.memory_manager.read_main_memory()
# 构建上下文
return self.context_manager.build_main_context(memory)
def _scan_project_memories(self):
"""扫描 .agents/memory/*.md 并解析 frontmatter 中的 name 和 description"""
try:
memory_dir = Path(self.project_path) / ".agents" / "memory"
except Exception:
return []
if not memory_dir.exists() or not memory_dir.is_dir():
return []
results = []
for md_file in sorted(memory_dir.glob("*.md")):
try:
text = md_file.read_text(encoding="utf-8")
name = None
description = None
if text.startswith("---"):
end = text.find("---", 3)
if end > 0:
frontmatter = text[3:end]
for line in frontmatter.strip().split("\n"):
line_stripped = line.strip()
if line_stripped.startswith("name:"):
name = line_stripped.split(":", 1)[1].strip()
elif line_stripped.startswith("description:"):
description = line_stripped.split(":", 1)[1].strip()
results.append({
"file": md_file.name,
"name": name or md_file.stem,
"description": description or "",
})
except Exception:
pass
return results
def _build_memory_system_content(self) -> str:
"""构建记忆系统的 prompt 内容"""
template = self.load_prompt("memory_system").strip()
if not template:
return ""
try:
global_memory = self.memory_manager.read_main_memory()
global_memory_text = global_memory.strip() if global_memory else ""
except Exception:
global_memory_text = ""
project_memories = self._scan_project_memories()
if project_memories:
lines = []
for m in project_memories:
desc = m.get("description", "")
if desc:
lines.append(f".agents/memory/{m['file']}{desc}")
else:
lines.append(f".agents/memory/{m['file']}")
project_memory_list = "\n".join(lines)
else:
project_memory_list = ""
result = template
if global_memory_text:
result = result.replace("{global_memory}", global_memory_text)
result = result.replace("[global_memory_empty]", "").replace("[/global_memory_empty]", "")
else:
result = result.replace("{global_memory}", "")
if project_memory_list:
result = result.replace("{project_memory_list}", project_memory_list)
result = result.replace("[project_memory_empty]", "").replace("[/project_memory_empty]", "")
else:
result = result.replace("{project_memory_list}", "")
return result.strip()
def _build_recent_conversations_message(self, limit: int = 10) -> Optional[str]:
"""构建最近对话提示(仅当前工作区)。"""
try:
manager = getattr(self.context_manager, "conversation_manager", None)
if not manager:
return None
current_id = getattr(self.context_manager, "current_conversation_id", None)
items = manager.get_recent_conversation_summaries(
limit=limit,
exclude_conversation_id=current_id,
first_message_max_chars=100,
)
if not items:
return None
lines: List[str] = []
for index, item in enumerate(items, start=1):
title = str(item.get("title") or "未命名对话").strip()
conv_id = str(item.get("id") or "").strip()
first_message = str(item.get("first_user_message") or "").strip() or "(无首条用户消息)"
updated_at = str(item.get("updated_at") or "").strip()
lines.append(f"{index}. [{conv_id}] 标题:{title}")
lines.append(f" 首条用户消息:{first_message}")
if updated_at:
lines.append(f" 更新时间:{updated_at}")
recent_text = "\n".join(lines)
template = self.load_prompt("recent_conversations").strip()
if template and "{recent_conversations}" in template:
return template.format(recent_conversations=recent_text)
if template:
return f"{template}\n\n{recent_text}"
return f"## 最近对话(仅当前工作区)\n\n{recent_text}"
except Exception:
return None
def _tool_calls_followed_by_tools(self, conversation: List[Dict], start_idx: int, tool_calls: List[Dict]) -> bool:
"""判断指定助手消息的工具调用是否拥有后续的工具响应。"""
if not tool_calls:
return False
expected_ids = [tc.get("id") for tc in tool_calls if tc.get("id")]
if not expected_ids:
return False
matched: Set[str] = set()
idx = start_idx + 1
total = len(conversation)
while idx < total and len(matched) < len(expected_ids):
next_conv = conversation[idx]
role = next_conv.get("role")
if role == "tool":
call_id = next_conv.get("tool_call_id")
if call_id in expected_ids:
matched.add(call_id)
else:
break
elif role in ("assistant", "user"):
break
idx += 1
return len(matched) == len(expected_ids)
def build_messages(self, context: Dict, user_input: str) -> List[Dict]:
"""构建消息列表(添加终端内容注入)"""
try:
file_tree_preview = (context.get("project_info", {}).get("file_tree") or "").splitlines()
write_host_workspace_debug(
"main_terminal.build_messages.context_snapshot",
terminal_id=id(self),
terminal_project_path=str(getattr(self, "project_path", "")),
context_project_path=str(getattr(getattr(self, "context_manager", None), "project_path", "")),
project_info_path=str(context.get("project_info", {}).get("path", "")),
file_tree_first_line=file_tree_preview[0] if file_tree_preview else "",
current_conversation_id=getattr(getattr(self, "context_manager", None), "current_conversation_id", None),
)
except Exception:
pass
# 根据当前模型多模态能力选择系统提示。system prompt 按对话冻结;
# 工具 schema 保持动态,由 define_tools 每轮根据真实状态生成。
current_model = getattr(self, "model_key", None)
prompt_name = "main_system_qwenvl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
model_key = getattr(self, "model_key", None)
prompt_replacements = get_model_prompt_replacements(model_key)
def _build_main_system_prompt() -> str:
system_prompt_template = self.load_prompt(prompt_name)
# main_system.txt / main_system_qwenvl.txt 仅使用 {model_description}
return system_prompt_template.format(
model_description=prompt_replacements.get("model_description", "")
)
system_prompt = self._get_or_init_frozen_prompt(
"frozen_main_system_prompt",
_build_main_system_prompt,
)
messages = [
{"role": "system", "content": system_prompt}
]
personalization_config = getattr(self.context_manager, "custom_personalization_config", None) or load_personalization_config(self.data_dir)
shallow_replace_enabled = bool(personalization_config.get("auto_shallow_compress_enabled", False)) if isinstance(personalization_config, dict) else False
# 顺序主prompt → 权限模式 → 执行环境 → 最近对话 → 个性化配置 → 工作区信息 → AGENTS.md → skills → 记忆 → 自定义 → 禁用提示
# 权限模式
permission_mode_message = self._get_or_init_frozen_mode_prompt(
"frozen_permission_prompt",
self._build_permission_mode_message,
)
if permission_mode_message:
messages.append({"role": "system", "content": permission_mode_message})
# 执行环境
execution_mode_message = self._get_or_init_frozen_mode_prompt(
"frozen_execution_prompt",
self._build_execution_mode_message,
)
if execution_mode_message:
messages.append({"role": "system", "content": execution_mode_message})
# 最近对话
def _build_recent_conversations_prompt() -> str:
recent_conversations_enabled = (
bool(personalization_config.get("recent_conversations_prompt_enabled", False))
if isinstance(personalization_config, dict)
else False
)
if not recent_conversations_enabled:
return ""
try:
recent_limit = int(
personalization_config.get(
"recent_conversations_prompt_limit",
RECENT_CONVERSATIONS_PROMPT_LIMIT_DEFAULT,
)
)
except Exception:
recent_limit = RECENT_CONVERSATIONS_PROMPT_LIMIT_DEFAULT
recent_limit = max(
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
min(RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX, recent_limit),
)
return self._build_recent_conversations_message(limit=recent_limit) or ""
recent_conversations_prompt = self._get_or_init_frozen_prompt(
"frozen_recent_conversations_prompt",
_build_recent_conversations_prompt,
)
if recent_conversations_prompt:
messages.append({"role": "system", "content": recent_conversations_prompt})
# 个性化配置
def _build_personalization_system_prompt() -> str:
personalization_block = build_personalization_prompt(personalization_config, include_header=False)
if not personalization_block:
return ""
personalization_template = self.load_prompt("personalization").strip()
if personalization_template and "{personalization_block}" in personalization_template:
return personalization_template.format(personalization_block=personalization_block)
if personalization_template:
return f"{personalization_template}\n{personalization_block}"
return personalization_block
personalization_text = self._get_or_init_frozen_prompt(
"frozen_personalization_prompt",
_build_personalization_system_prompt,
)
if personalization_text:
messages.append({"role": "system", "content": personalization_text})
# 工作区信息
workspace_system = self._get_or_init_frozen_prompt(
"frozen_workspace_prompt",
lambda: self.context_manager._build_workspace_system_message(context) or "",
)
if workspace_system:
messages.append({"role": "system", "content": workspace_system})
# AGENTS.md
def _build_agents_md_prompt() -> str:
agents_md_inject_enabled = bool(personalization_config.get("agents_md_auto_inject", False)) if isinstance(personalization_config, dict) else False
if not agents_md_inject_enabled:
return ""
agents_md_content = self._load_agents_md_content()
if not agents_md_content:
return ""
agents_md_template = self.load_prompt("agents_md_inject").strip()
if agents_md_template and "{{AGENTS_MD_CONTENT}}" in agents_md_template:
return agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content)
return f"【AGENTS.md 项目规范】\n\n{agents_md_content}\n\n---\n请注意:以上规范来自工作区根目录的 AGENTS.md 文件,若有冲突请以 AGENTS.md 为准。"
agents_md_text = self._get_or_init_frozen_prompt(
"frozen_agents_md_prompt",
_build_agents_md_prompt,
)
if agents_md_text:
messages.append({"role": "system", "content": agents_md_text})
# skills 列表
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
enabled_skills = merge_enabled_skills(
personalization_config.get("enabled_skills") if isinstance(personalization_config, dict) else None,
skills_catalog,
personalization_config.get("skills_catalog_snapshot") if isinstance(personalization_config, dict) else None,
)
def _build_skills_system_prompt() -> str:
skills_template = self.load_prompt("skills_system").strip()
skills_list = build_skills_list(skills_catalog, enabled_skills)
return build_skills_prompt(skills_template, skills_list) or ""
skills_prompt = self._get_or_init_frozen_prompt(
"frozen_skills_prompt",
_build_skills_system_prompt,
)
if skills_prompt:
messages.append({"role": "system", "content": skills_prompt})
# 记忆系统(总体长期记忆 + 项目记忆)
def _build_memory_system_prompt() -> str:
return self._build_memory_system_content()
memory_prompt = self._get_or_init_frozen_prompt(
"frozen_memory_prompt",
_build_memory_system_prompt,
)
if memory_prompt:
messages.append({"role": "system", "content": memory_prompt})
# API 自定义 system prompt
custom_system_prompt = self._get_or_init_frozen_prompt(
"frozen_custom_system_prompt",
lambda: (
getattr(self.context_manager, "custom_system_prompt", "").strip()
if isinstance(getattr(self.context_manager, "custom_system_prompt", None), str)
else ""
),
)
if custom_system_prompt:
messages.append({"role": "system", "content": custom_system_prompt})
# 禁用工具提示
disabled_notice = self._get_or_init_frozen_prompt(
"frozen_disabled_tools_prompt",
lambda: self._format_disabled_tool_notice() or "",
)
if disabled_notice:
messages.append({
"role": "system",
"content": disabled_notice
})
# 添加对话历史保留完整结构包括tool_calls和tool消息
conversation = context["conversation"]
replaced_tool_count = 0
deep_compacted_skipped = 0
for idx, conv in enumerate(conversation):
metadata = conv.get("metadata") or {}
# 深压缩:被标记为 deep_compacted 的消息(整段已压缩前缀)原文保留在历史中用于展示,
# 但构建请求时整体跳过,不纳入上下文。由于按连续前缀整体标记,
# 不会出现 assistant.tool_calls 与其 tool 响应被拆散导致的配对悬空。
if metadata.get("deep_compacted"):
deep_compacted_skipped += 1
continue
if conv["role"] == "assistant":
# Assistant消息可能包含工具调用
message = {
"role": conv["role"],
"content": conv["content"]
}
# 对于思考模式(如 DeepSeek thinkingassistant 历史消息中的
# reasoning_content 即使为空字符串也需要原样回传,否则下一轮可能被
# API 判定为“未回传 reasoning_content”并返回 400。
if "reasoning_content" in conv:
message["reasoning_content"] = conv.get("reasoning_content", "")
# 如果有工具调用信息,添加到消息中
tool_calls = conv.get("tool_calls") or []
if tool_calls and self._tool_calls_followed_by_tools(conversation, idx, tool_calls):
message["tool_calls"] = tool_calls
messages.append(message)
elif conv["role"] == "tool":
if shallow_replace_enabled and metadata.get("auto_shallow_compacted"):
messages.append({
"role": "tool",
"content": AUTO_SHALLOW_PLACEHOLDER,
"tool_call_id": conv.get("tool_call_id", ""),
"name": conv.get("name", "")
})
replaced_tool_count += 1
continue
# Tool消息需要保留完整结构
images = conv.get("images") or metadata.get("images") or []
videos = conv.get("videos") or metadata.get("videos") or []
media_refs = conv.get("media_refs") or metadata.get("media_refs") or []
content_value = conv.get("content")
if isinstance(content_value, list):
content_payload = content_value
elif images or videos or media_refs:
content_payload = self.context_manager._build_content_with_images(
content_value,
images,
videos,
media_refs=media_refs,
)
else:
content_payload = content_value
message = {
"role": "tool",
"content": content_payload,
"tool_call_id": conv.get("tool_call_id", ""),
"name": conv.get("name", "")
}
messages.append(message)
else:
# User 或普通 System 消息
images = conv.get("images") or metadata.get("images") or []
videos = conv.get("videos") or metadata.get("videos") or []
media_refs = conv.get("media_refs") or metadata.get("media_refs") or []
content_payload = (
self.context_manager._build_content_with_images(
conv["content"],
images,
videos,
media_refs=media_refs,
)
if (images or videos or media_refs) else conv["content"]
)
# 调试:记录所有 system 消息
if conv["role"] == "system":
logger.info(f"[DEBUG build_messages] 添加 system 消息: content前50字={conv['content'][:50]}")
messages.append({
"role": conv["role"],
"content": content_payload
})
# 当前用户输入已经在conversation中了不需要重复添加
if shallow_replace_enabled:
print(f"[ContextCompression] build_messages 替换tool占位符: {replaced_tool_count}")
if deep_compacted_skipped:
print(f"[ContextCompression] build_messages 跳过已深压缩消息: {deep_compacted_skipped}")
return messages
def _load_agents_md_content(self) -> Optional[str]:
"""加载工作区根目录的 AGENTS.md 文件内容。
如果存在多个 AGENTS.md 文件,返回最新更新的那一个。
"""
try:
project_path = Path(self.project_path)
if not project_path.exists():
return None
# 查找所有 AGENTS.md 文件
agents_md_files = list(project_path.rglob("AGENTS.md"))
if not agents_md_files:
return None
# 找到最新更新的文件
latest_file = max(agents_md_files, key=lambda p: p.stat().st_mtime)
# 读取文件内容
content = latest_file.read_text(encoding='utf-8')
return content.strip() if content else None
except Exception as exc:
logger.warning(f"[AGENTS.md] 读取失败: {exc}")
return None
def load_prompt(self, name: str) -> str:
"""加载提示模板"""
prompt_file = Path(PROMPTS_DIR) / f"{name}.txt"
if prompt_file.exists():
with open(prompt_file, 'r', encoding='utf-8') as f:
return f.read()
return "你是一个AI助手。"