fix(multi-agent): 修复压缩404、prompt冻结串扰、子智能体状态串扰及/multiagent/new跳转问题
This commit is contained in:
parent
ce92697181
commit
351cd311c2
19
AGENTS.md
19
AGENTS.md
@ -201,6 +201,25 @@
|
|||||||
1. **meta 部分只放参数**:`tool-result-meta` 仅用于展示工具调用的参数、配置、状态标识等元信息。例如:状态、ID、路径、任务描述、超时时间等。禁止在 meta 区域放置执行结果、统计摘要、输出内容等。
|
1. **meta 部分只放参数**:`tool-result-meta` 仅用于展示工具调用的参数、配置、状态标识等元信息。例如:状态、ID、路径、任务描述、超时时间等。禁止在 meta 区域放置执行结果、统计摘要、输出内容等。
|
||||||
2. **content 部分只放结果**:`tool-result-content` 仅用于展示工具执行后的结果、输出、统计、总结等。例如:文件内容、命令输出、搜索命中、执行统计(工作时间 / 调用次数 / 工具次数)、最终回复摘要等。禁止在 content 区域重复展示工具参数。
|
2. **content 部分只放结果**:`tool-result-content` 仅用于展示工具执行后的结果、输出、统计、总结等。例如:文件内容、命令输出、搜索命中、执行统计(工作时间 / 调用次数 / 工具次数)、最终回复摘要等。禁止在 content 区域重复展示工具参数。
|
||||||
|
|
||||||
|
## 5.7) 调试输出规范(强制)
|
||||||
|
|
||||||
|
> 约束级别:需要临时加调试日志排查问题时**必须遵守**。
|
||||||
|
|
||||||
|
### 前端调试日志
|
||||||
|
|
||||||
|
- **统一筛选词**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
|
||||||
|
- **控制数量与时机**:
|
||||||
|
- 禁止一次性输出上百上千条日志刷屏。
|
||||||
|
- 只在关键路径(入口、分支判断、状态变化、导航动作)打印,避免在循环、高频事件、每帧渲染中输出。
|
||||||
|
- 需要大量结构化数据时,优先用 `console.group` / `console.table` 或对象快照,而不是逐条打印。
|
||||||
|
- **用完即清**:问题定位后应及时删除或注释掉临时调试日志,不要长期留在代码里。
|
||||||
|
|
||||||
|
### 后端调试日志
|
||||||
|
|
||||||
|
- **必须写入文件**:后端调试信息禁止直接 `print` 到终端刷屏,必须写入日志文件。
|
||||||
|
- **复用现有 logger**:优先复用项目已有 logger(如 `utils/logger.py`、`modules/multi_agent/debug_logger.py` 等),按模块落到 `~/.astrion/astrion/<mode>/logs/` 下。
|
||||||
|
- **关键状态转换必打**:多智能体、子智能体、任务轮询等复杂链路应在关键状态转换点写结构化日志,便于复现问题后按时间线追溯。
|
||||||
|
|
||||||
## 6) Git 工作流(开发 + Review)
|
## 6) Git 工作流(开发 + Review)
|
||||||
|
|
||||||
### 6.1 核心原则
|
### 6.1 核心原则
|
||||||
|
|||||||
@ -117,15 +117,29 @@ class MessagesMixin:
|
|||||||
model_key = getattr(self, "model_key", None)
|
model_key = getattr(self, "model_key", None)
|
||||||
prompt_replacements = get_model_prompt_replacements(model_key)
|
prompt_replacements = get_model_prompt_replacements(model_key)
|
||||||
|
|
||||||
|
is_multi_agent_mode = bool(getattr(self, "multi_agent_mode", False))
|
||||||
|
|
||||||
def _build_main_system_prompt() -> str:
|
def _build_main_system_prompt() -> str:
|
||||||
|
if is_multi_agent_mode:
|
||||||
|
# 多智能体模式下主 prompt 使用 Team Leader 专属模板
|
||||||
|
try:
|
||||||
|
from modules.multi_agent.prompts import _load_template
|
||||||
|
return _load_template("master")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"[messages] 加载多智能体主 prompt 失败: {exc}")
|
||||||
system_prompt_template = self.load_prompt(prompt_name)
|
system_prompt_template = self.load_prompt(prompt_name)
|
||||||
# main_system.txt / main_system_vl.txt 仅使用 {model_description}
|
# main_system.txt / main_system_vl.txt 仅使用 {model_description}
|
||||||
return system_prompt_template.format(
|
return system_prompt_template.format(
|
||||||
model_description=prompt_replacements.get("model_description", "")
|
model_description=prompt_replacements.get("model_description", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
main_system_frozen_key = (
|
||||||
|
"frozen_main_system_prompt_multi_agent"
|
||||||
|
if is_multi_agent_mode
|
||||||
|
else "frozen_main_system_prompt"
|
||||||
|
)
|
||||||
system_prompt = self._get_or_init_frozen_prompt(
|
system_prompt = self._get_or_init_frozen_prompt(
|
||||||
"frozen_main_system_prompt",
|
main_system_frozen_key,
|
||||||
_build_main_system_prompt,
|
_build_main_system_prompt,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -225,12 +239,19 @@ class MessagesMixin:
|
|||||||
return agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content)
|
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 为准。"
|
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(
|
# AGENTS.md 注入开关可能变化,只在开启时生成并缓存,避免关闭时把空字符串冻住
|
||||||
"frozen_agents_md_prompt",
|
agents_md_inject_enabled = (
|
||||||
_build_agents_md_prompt,
|
bool(personalization_config.get("agents_md_auto_inject", False))
|
||||||
|
if isinstance(personalization_config, dict)
|
||||||
|
else False
|
||||||
)
|
)
|
||||||
if agents_md_text:
|
if agents_md_inject_enabled:
|
||||||
messages.append({"role": "system", "content": agents_md_text})
|
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 列表
|
||||||
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
|
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
|
||||||
@ -280,17 +301,8 @@ class MessagesMixin:
|
|||||||
lambda: self._format_disabled_tool_notice() or "",
|
lambda: self._format_disabled_tool_notice() or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 多智能体模式提示词:当会话 metadata.multi_agent_mode 为真时追加
|
# 多智能体模式额外提示词:可用的子智能体角色(动态 prompt,第一个用户消息后冻结)
|
||||||
if getattr(self, "multi_agent_mode", False):
|
if getattr(self, "multi_agent_mode", False):
|
||||||
try:
|
|
||||||
from modules.multi_agent.prompts import build_multi_agent_master_prompt
|
|
||||||
multi_agent_prompt = build_multi_agent_master_prompt(self.project_path, base="")
|
|
||||||
if multi_agent_prompt:
|
|
||||||
messages.append({"role": "system", "content": multi_agent_prompt})
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning(f"[messages] 注入多智能体 prompt 失败: {exc}")
|
|
||||||
|
|
||||||
# 可用的子智能体角色(动态 prompt,第一个用户消息后冻结)
|
|
||||||
try:
|
try:
|
||||||
available_agents_prompt = self._get_or_init_frozen_prompt(
|
available_agents_prompt = self._get_or_init_frozen_prompt(
|
||||||
"frozen_available_agents_prompt",
|
"frozen_available_agents_prompt",
|
||||||
|
|||||||
@ -221,7 +221,12 @@ class ModeMixin:
|
|||||||
conv_id = getattr(cm, "current_conversation_id", None) if cm else None
|
conv_id = getattr(cm, "current_conversation_id", None) if cm else None
|
||||||
if cm and conv_id:
|
if cm and conv_id:
|
||||||
try:
|
try:
|
||||||
cm.conversation_manager.update_conversation_metadata(conv_id, {key: built})
|
target_manager = (
|
||||||
|
cm._get_conversation_manager_for_id(conv_id)
|
||||||
|
if hasattr(cm, "_get_conversation_manager_for_id")
|
||||||
|
else cm.conversation_manager
|
||||||
|
)
|
||||||
|
target_manager.update_conversation_metadata(conv_id, {key: built})
|
||||||
if isinstance(cm.conversation_metadata, dict):
|
if isinstance(cm.conversation_metadata, dict):
|
||||||
cm.conversation_metadata[key] = built
|
cm.conversation_metadata[key] = built
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -1675,13 +1675,26 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s
|
|||||||
pass
|
pass
|
||||||
conversation_id = terminal.context_manager.current_conversation_id
|
conversation_id = terminal.context_manager.current_conversation_id
|
||||||
data = manager.get_overview(conversation_id=conversation_id)
|
data = manager.get_overview(conversation_id=conversation_id)
|
||||||
|
|
||||||
|
# 传统模式子智能体列表必须排除多智能体任务,避免 /new 等无当前对话场景
|
||||||
|
# 把最近一次多智能体模式的 running/idle 子智能体串过来。
|
||||||
|
def _is_multi_agent_task(item: dict) -> bool:
|
||||||
|
task_id = item.get("task_id")
|
||||||
|
raw_task = manager.tasks.get(task_id) if task_id else None
|
||||||
|
return bool(raw_task.get("multi_agent_mode")) if isinstance(raw_task, dict) else False
|
||||||
|
|
||||||
|
data = [item for item in data if not _is_multi_agent_task(item)]
|
||||||
|
|
||||||
# 仅在未绑定具体对话时(如全新会话尚未创建 conversation_id),才回退为全局运行态;
|
# 仅在未绑定具体对话时(如全新会话尚未创建 conversation_id),才回退为全局运行态;
|
||||||
# 否则只显示当前对话关联的子智能体,避免新对话看到其他对话的任务。
|
# 否则只显示当前对话关联的子智能体,避免新对话看到其他对话的任务。
|
||||||
if not data and not conversation_id:
|
if not data and not conversation_id:
|
||||||
all_overview = manager.get_overview(conversation_id=None)
|
all_overview = manager.get_overview(conversation_id=None)
|
||||||
if all_overview:
|
if all_overview:
|
||||||
terminal_statuses = TERMINAL_STATUSES.union({"terminated"})
|
terminal_statuses = TERMINAL_STATUSES.union({"terminated"})
|
||||||
running_only = [item for item in all_overview if item.get("status") not in terminal_statuses]
|
running_only = [
|
||||||
|
item for item in all_overview
|
||||||
|
if item.get("status") not in terminal_statuses and not _is_multi_agent_task(item)
|
||||||
|
]
|
||||||
if running_only:
|
if running_only:
|
||||||
data = running_only
|
data = running_only
|
||||||
debug_log(
|
debug_log(
|
||||||
|
|||||||
@ -312,7 +312,12 @@ async def run_deep_compression(
|
|||||||
sender=None,
|
sender=None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
cm = web_terminal.context_manager
|
cm = web_terminal.context_manager
|
||||||
conv_data = cm.conversation_manager.load_conversation(conversation_id)
|
target_manager = (
|
||||||
|
cm._get_conversation_manager_for_id(conversation_id)
|
||||||
|
if hasattr(cm, "_get_conversation_manager_for_id")
|
||||||
|
else cm.conversation_manager
|
||||||
|
)
|
||||||
|
conv_data = target_manager.load_conversation(conversation_id)
|
||||||
if not conv_data:
|
if not conv_data:
|
||||||
return {"success": False, "error": f"对话不存在: {conversation_id}"}
|
return {"success": False, "error": f"对话不存在: {conversation_id}"}
|
||||||
|
|
||||||
@ -419,7 +424,7 @@ async def run_deep_compression(
|
|||||||
# 关键:重置 current_context_tokens,避免自动压缩续接后阈值判断仍读到压缩前的大值而陷入死循环。
|
# 关键:重置 current_context_tokens,避免自动压缩续接后阈值判断仍读到压缩前的大值而陷入死循环。
|
||||||
# 真实上下文长度会在下一次 API 响应后被重新写入。
|
# 真实上下文长度会在下一次 API 响应后被重新写入。
|
||||||
try:
|
try:
|
||||||
cm.conversation_manager.update_token_statistics(
|
target_manager.update_token_statistics(
|
||||||
conversation_id,
|
conversation_id,
|
||||||
input_tokens=0,
|
input_tokens=0,
|
||||||
output_tokens=0,
|
output_tokens=0,
|
||||||
@ -484,7 +489,7 @@ async def run_deep_compression(
|
|||||||
}
|
}
|
||||||
for frozen_key in REBUILD_FROZEN_KEYS:
|
for frozen_key in REBUILD_FROZEN_KEYS:
|
||||||
meta_updates[frozen_key] = None
|
meta_updates[frozen_key] = None
|
||||||
cm.conversation_manager.update_conversation_metadata(conversation_id, meta_updates)
|
target_manager.update_conversation_metadata(conversation_id, meta_updates)
|
||||||
# 同步内存中的 metadata,清除 frozen 缓存
|
# 同步内存中的 metadata,清除 frozen 缓存
|
||||||
try:
|
try:
|
||||||
if getattr(cm, "current_conversation_id", None) == conversation_id and isinstance(cm.conversation_metadata, dict):
|
if getattr(cm, "current_conversation_id", None) == conversation_id and isinstance(cm.conversation_metadata, dict):
|
||||||
|
|||||||
@ -134,10 +134,12 @@ export const syncMethods = {
|
|||||||
|
|
||||||
const pathFragment = this.stripConversationPrefix(data.conversation_id);
|
const pathFragment = this.stripConversationPrefix(data.conversation_id);
|
||||||
const currentPath = window.location.pathname.replace(/^\/+/, '');
|
const currentPath = window.location.pathname.replace(/^\/+/, '');
|
||||||
|
const urlPrefix = this.multiAgentMode ? '/multiagent/' : '/';
|
||||||
|
const targetPath = `${urlPrefix}${pathFragment}`;
|
||||||
if (data.created) {
|
if (data.created) {
|
||||||
history.pushState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
|
history.pushState({ conversationId: data.conversation_id }, '', targetPath);
|
||||||
} else if (currentPath !== pathFragment) {
|
} else if (currentPath !== pathFragment) {
|
||||||
history.replaceState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
|
history.replaceState({ conversationId: data.conversation_id }, '', targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,8 @@ export const routeMethods = {
|
|||||||
this.multiAgentMode = true;
|
this.multiAgentMode = true;
|
||||||
this.currentConversationId = null;
|
this.currentConversationId = null;
|
||||||
this.currentConversationTitle = '多智能体模式';
|
this.currentConversationTitle = '多智能体模式';
|
||||||
|
this.logMessageState('bootstrapRoute:clear-messages-for-multiagent-new');
|
||||||
|
this.messages = [];
|
||||||
this.titleReady = true;
|
this.titleReady = true;
|
||||||
this.suppressTitleTyping = false;
|
this.suppressTitleTyping = false;
|
||||||
this.startTitleTyping('多智能体模式', { animate: false });
|
this.startTitleTyping('多智能体模式', { animate: false });
|
||||||
@ -47,38 +49,6 @@ export const routeMethods = {
|
|||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// 重建失败不阻断主流程
|
// 重建失败不阻断主流程
|
||||||
}
|
}
|
||||||
// 多智能体模式下自动创建一个带 metadata.multi_agent_mode=true 的新对话
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/multiagent/conversations', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({})
|
|
||||||
});
|
|
||||||
const result = await resp.json();
|
|
||||||
if (result && result.success && result.conversation_id) {
|
|
||||||
this.currentConversationId = result.conversation_id;
|
|
||||||
// 拉取完整会话信息
|
|
||||||
try {
|
|
||||||
const loadResp = await fetch(`/api/conversations/${result.conversation_id}/load`, { method: 'PUT' });
|
|
||||||
const loadResult = await loadResp.json();
|
|
||||||
if (loadResult.success) {
|
|
||||||
if (typeof loadResult.run_mode === 'string') {
|
|
||||||
this.runMode = loadResult.run_mode;
|
|
||||||
this.thinkingMode = typeof loadResult.thinking_mode === 'boolean' ? loadResult.thinking_mode : loadResult.run_mode !== 'fast';
|
|
||||||
}
|
|
||||||
if (typeof loadResult.model_key === 'string' && loadResult.model_key) this.modelSet(loadResult.model_key);
|
|
||||||
this.currentConversationTitle = loadResult.title || '多智能体模式';
|
|
||||||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
|
||||||
history.replaceState({ conversationId: result.conversation_id }, '', `/multiagent/${result.conversation_id.replace(/^conv_/, '')}`);
|
|
||||||
this.logMessageState('bootstrapRoute:multi-agent-loaded');
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// 加载失败也继续,主对话 已创建
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[multiagent] 初始化多智能体对话失败:', error);
|
|
||||||
}
|
|
||||||
await this.restoreComposerDraftState('bootstrap-route:multiagent');
|
await this.restoreComposerDraftState('bootstrap-route:multiagent');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -214,7 +184,7 @@ export const routeMethods = {
|
|||||||
},
|
},
|
||||||
isExplicitNewConversationRoute() {
|
isExplicitNewConversationRoute() {
|
||||||
const normalizedPath = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
const normalizedPath = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||||
return normalizedPath === 'new';
|
return normalizedPath === 'new' || normalizedPath === 'multiagent/new' || normalizedPath === 'multiagent';
|
||||||
},
|
},
|
||||||
stripConversationPrefix(conversationId) {
|
stripConversationPrefix(conversationId) {
|
||||||
if (!conversationId) return '';
|
if (!conversationId) return '';
|
||||||
|
|||||||
@ -321,10 +321,13 @@ export const socketMethods = {
|
|||||||
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
|
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
|
||||||
const resumeConversationId = statusConversationId || runningConversationId;
|
const resumeConversationId = statusConversationId || runningConversationId;
|
||||||
const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId;
|
const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId;
|
||||||
|
const currentPath = window.location.pathname.replace(/^\/+/, '');
|
||||||
|
const isMultiAgentNewRoute = currentPath === 'multiagent/new' || currentPath === 'multiagent';
|
||||||
|
|
||||||
if (
|
if (
|
||||||
resumeConversationId &&
|
resumeConversationId &&
|
||||||
!this.currentConversationId &&
|
!this.currentConversationId &&
|
||||||
|
!isMultiAgentNewRoute &&
|
||||||
(!isExplicitNewRoute || shouldResumeOnNewRoute)
|
(!isExplicitNewRoute || shouldResumeOnNewRoute)
|
||||||
) {
|
) {
|
||||||
this.skipConversationHistoryReload = true;
|
this.skipConversationHistoryReload = true;
|
||||||
|
|||||||
@ -804,7 +804,6 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const convId = data.conversation_id;
|
const convId = data.conversation_id;
|
||||||
console.log('[conv-trace] socket:conversation_resolved', data);
|
|
||||||
|
|
||||||
// 初始化期间不修改 currentConversationId,避免与 bootstrapRoute 冲突
|
// 初始化期间不修改 currentConversationId,避免与 bootstrapRoute 冲突
|
||||||
if (ctx.initialRouteResolved) {
|
if (ctx.initialRouteResolved) {
|
||||||
@ -815,10 +814,12 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.promoteConversationToTop(convId);
|
ctx.promoteConversationToTop(convId);
|
||||||
const pathFragment = ctx.stripConversationPrefix(convId);
|
const pathFragment = ctx.stripConversationPrefix(convId);
|
||||||
const currentPath = window.location.pathname.replace(/^\/+/, '');
|
const currentPath = window.location.pathname.replace(/^\/+/, '');
|
||||||
|
const urlPrefix = ctx.multiAgentMode ? '/multiagent/' : '/';
|
||||||
|
const targetPath = `${urlPrefix}${pathFragment}`;
|
||||||
if (data.created) {
|
if (data.created) {
|
||||||
history.pushState({ conversationId: convId }, '', `/${pathFragment}`);
|
history.pushState({ conversationId: convId }, '', targetPath);
|
||||||
} else if (currentPath !== pathFragment) {
|
} else if (currentPath !== pathFragment) {
|
||||||
history.replaceState({ conversationId: convId }, '', `/${pathFragment}`);
|
history.replaceState({ conversationId: convId }, '', targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -102,7 +102,12 @@ class CompressionMixin:
|
|||||||
self.conversation_metadata[k] = v
|
self.conversation_metadata[k] = v
|
||||||
if self.current_conversation_id:
|
if self.current_conversation_id:
|
||||||
try:
|
try:
|
||||||
self.conversation_manager.update_conversation_metadata(self.current_conversation_id, updates)
|
target_manager = (
|
||||||
|
self._get_conversation_manager_for_id(self.current_conversation_id)
|
||||||
|
if hasattr(self, "_get_conversation_manager_for_id")
|
||||||
|
else self.conversation_manager
|
||||||
|
)
|
||||||
|
target_manager.update_conversation_metadata(self.current_conversation_id, updates)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"[ContextCompression] 更新压缩状态失败: {exc}")
|
print(f"[ContextCompression] 更新压缩状态失败: {exc}")
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user