feat: 升级至 v0.3.16 — 工单修复 51 项 + 审查修复 30 项(安全加固/适配器/工具系统/数据层/前端)
This commit is contained in:
@@ -125,6 +125,22 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
*/
|
||||
setAdapter(adapter: IMetonaProviderAdapter): void {
|
||||
this.adapter = adapter;
|
||||
// #3 修复: 热切换 adapter 时同步 contextWindow,防止压缩阈值计算错误
|
||||
this.syncContextWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* #3 修复: 从 adapter 同步 contextWindow 到 Engine 配置
|
||||
*
|
||||
* Engine 的 DEFAULT_CONFIG.contextWindow 硬编码为 128_000,但各 Provider 实际支持的
|
||||
* 上下文窗口差异巨大(DeepSeek 64K / Agnes 1M / Ollama 4096)。
|
||||
* 不同步会导致压缩阈值(compressionThreshold * contextWindow)计算错误。
|
||||
*/
|
||||
private syncContextWindow(): void {
|
||||
const adapterCtx = this.adapter.getContextWindow?.();
|
||||
if (adapterCtx && adapterCtx > 0) {
|
||||
this.config.contextWindow = adapterCtx;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,6 +197,9 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
if (this.adapter.setAbortSignal) {
|
||||
this.adapter.setAbortSignal(this.abortController.signal);
|
||||
}
|
||||
// #3 修复: 每次 runStream 开始时从 adapter 同步 contextWindow,
|
||||
// 确保压缩阈值基于当前 Provider 的实际上下文窗口
|
||||
this.syncContextWindow();
|
||||
this.eventSeq = 0;
|
||||
// v0.3.0: 重置工具调用历史(用于死循环检测)
|
||||
this.toolCallHistory = [];
|
||||
@@ -302,6 +321,18 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
return this.adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* #4 修复: 恢复 adapter 的 abort signal
|
||||
*
|
||||
* SubEngine 共享主 Engine 的 adapter 时,SubEngine 会覆盖 adapter 的 abort signal。
|
||||
* SubEngine 完成后,主 Engine 需调用此方法恢复自己的 signal,否则后续 fetch 无法被中断。
|
||||
*/
|
||||
restoreAbortSignal(): void {
|
||||
if (this.abortController && this.adapter.setAbortSignal) {
|
||||
this.adapter.setAbortSignal(this.abortController.signal);
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取工作空间路径(供 SubAgent 继承) */
|
||||
getWorkspacePath(): string {
|
||||
return this.workspacePath;
|
||||
@@ -331,14 +362,29 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
*/
|
||||
async waitForAbort(timeoutMs: number = 5_000): Promise<boolean> {
|
||||
if (!this.currentRunPromise) return true;
|
||||
// #29 修复: 原实现 Promise.race 永不抛错(currentRunPromise.catch 已吞异常),
|
||||
// 导致 try/catch 永远走不到,总是返回 true。调用方误以为 abort 完成,
|
||||
// 立即重发的新消息会卡在 runStream 的 currentRunPromise 等待中。
|
||||
// 改为用 timedOut 标志检测超时,超时返回 false 让调用方提示用户"上一次操作未完成"。
|
||||
let timedOut = false;
|
||||
// 审查修复: 用 finally 块清理 timer,避免 currentRunPromise 先完成时 setTimeout 残留
|
||||
let timerHandle: NodeJS.Timeout | undefined;
|
||||
const timer = new Promise<void>((resolve) => {
|
||||
timerHandle = setTimeout(() => {
|
||||
timedOut = true;
|
||||
resolve();
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
this.currentRunPromise.catch(() => {}),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
|
||||
timer,
|
||||
]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
return !timedOut; // 超时返回 false,run 正常结束返回 true
|
||||
} finally {
|
||||
// 审查修复: 无论 race 谁先完成,都清理 timer 防止事件循环残留
|
||||
if (timerHandle) clearTimeout(timerHandle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,18 +1104,30 @@ export class AgentLoopEngine extends EventEmitter {
|
||||
if (!summary) return null;
|
||||
|
||||
const summaryMessage: MetonaMessage = {
|
||||
// CE-1 修复: 使用 role: 'user' 而非 'system'
|
||||
// adapter 的 buildOpenAICompatibleMessages 会过滤所有 role !== 'system' 的消息,
|
||||
// 只保留 systemPrompt 构建的 system 消息。如果用 'system',摘要会被丢弃,压缩无效。
|
||||
// 改为 'user' 后 LLM 会看到:system(systemPrompt) → user(摘要) → ...toKeep
|
||||
role: 'user',
|
||||
// #30 修复: 改用 assistant 角色注入摘要,避免语义混淆
|
||||
// 原 CE-1 修复用 'user' 角色,会导致 LLM 将摘要误视为新的用户指令,
|
||||
// 可能基于"Summary of previous conversation"字面意思执行奇怪操作。
|
||||
// 工单建议方案 A(system 角色)不可行:buildOpenAICompatibleMessages 会
|
||||
// 过滤所有 role === 'system' 的消息(只保留 systemPrompt 构建的 system 消息),
|
||||
// 用 system 角色摘要会被丢弃,压缩无效。
|
||||
// 采用 assistant 角色:既不会被过滤,又保持语义中立(摘要是 AI 生成的总结),
|
||||
// LLM 不会将其视为新的用户指令。
|
||||
role: 'assistant',
|
||||
content: `[Context Summary] The following is a summary of earlier conversation:\n\n${summary}`,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
log.info(`[AgentLoop] Context compressed: ${toCompress.length} messages → 1 summary, kept ${toKeep.length} recent`);
|
||||
|
||||
return [summaryMessage, ...toKeep];
|
||||
// 审查修复: #30 修复将摘要改为 assistant 角色,可能导致连续两个 assistant 消息
|
||||
// (summary + 带 tool_calls 的 assistant),部分 Provider 会返回 400。
|
||||
// 插入占位 user 消息保证对话流清晰。
|
||||
return [
|
||||
summaryMessage,
|
||||
// 审查修复: 插入占位 user 消息避免连续 assistant 消息
|
||||
{ role: 'user', content: '[Continue from the summary above.]', timestamp: Date.now() },
|
||||
...toKeep,
|
||||
];
|
||||
} catch (error) {
|
||||
log.warn('[AgentLoop] Context compression failed, keeping original messages:', (error as Error).message);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user