v0.16.10: 最小必要性清理 — 删除非必要校验和注入提示

按"让 AI 自己判断"原则,删除所有"工程师判断"性质的校验代码和注入提示,
只保留安全防护(路径沙箱/命令安全/参数消毒)和必要的上下文管理(压缩/截断)。

删除项:
- completion-gate.ts 整个文件(notThinking/toolResultReview/contextEfficiency/planModeCompletion)
- verifyToolResult 工具结果核验函数 + TOOLS_NEED_VERIFY 常量
- 跨轮次死循环检测器(recordLoopSignature/detectLoopDeadlock/resetLoopDeadlockDetector)
- R77 checkRateLimit 速率限制 + R87 isToolCircuitBroken 熔断器 + R104 isDuplicateToolResult 去重
- R56 目标对齐验证 + R63 速率限制 + R87 熔断器 + R104 去重检测 + R119 优先级排序
- agent-metrics recordCompletionGate + completionGatePassed + avgCompletionScore 相关代码
- context-manager 低价值关键词黑名单 + 快速摘要改用 user role
- LoopContext 的 completionGateFailCount/verifyWarnings 字段

修复项:
- R76 路径沙箱 replace bug:用 startsWith 前缀锚定替代 replace,避免子串误判
- R28 命令注入检测:缩小匹配范围,仅拦截命令替换中包含危险命令的情况

总计 13 文件变更,+46/-1124 行
This commit is contained in:
紫影233
2026-07-16 16:44:04 +08:00
parent a4b82b45bc
commit 80581deb37
13 changed files with 46 additions and 1124 deletions
+3 -3
View File
@@ -14,7 +14,7 @@
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v0.16.9-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/version-v0.16.10-E8734A?style=flat-square" alt="version">
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license">
@@ -253,7 +253,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
产出:`release/Metona Ollama Setup v0.16.9.exe`
产出:`release/Metona Ollama Setup v0.16.10.exe`
## 🛠️ 常用命令
@@ -501,7 +501,7 @@ npm start
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
```
Output: `release/Metona Ollama Setup v0.16.9.exe`
Output: `release/Metona Ollama Setup v0.16.10.exe`
## 🛠️ Common Commands
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "metona-ollama-desktop",
"version": "0.16.9",
"version": "0.16.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "metona-ollama-desktop",
"version": "0.16.9",
"version": "0.16.10",
"license": "MIT",
"dependencies": {
"ffmpeg-static": "^5.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "metona-ollama-desktop",
"version": "0.16.9",
"version": "0.16.10",
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
"main": "dist/main/main.js",
"author": "thzxx",
+1 -1
View File
@@ -101,7 +101,7 @@ export function createMenu(): void {
dialog.showMessageBox(mainWindow!, {
type: 'info',
title: '关于 Metona Ollama',
message: 'Metona Ollama Desktop v0.16.9',
message: 'Metona Ollama Desktop v0.16.10',
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
icon: getIconPath()
});
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="header-left">
<img class="logo" src="./assets/icons/llama.png" alt="logo" />
<span class="app-title">Metona Ollama</span>
<span class="app-version">v0.16.9</span>
<span class="app-version">v0.16.10</span>
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
+13 -298
View File
@@ -18,21 +18,14 @@ import {
} from './tool-registry.js';
import {
compactOldToolResult,
setUserGoal,
resetAllSafetyState,
checkRateLimit,
classifyError,
calculateBackoff,
validatePathSandbox,
isToolCircuitBroken,
recordToolFailure,
recordToolSuccess,
// R88: 工具结果元数据
addResultMetadata,
// R97: 错误模式学习
recordErrorPattern,
// R104: 工具结果去重
isDuplicateToolResult,
// R109: 工具参数消毒
sanitizeToolArgs,
// R113: 命令安全检查
@@ -60,9 +53,8 @@ import {
// R100: Token 使用统计报告
generateTokenReport, formatTokenReport,
} from './context-manager.js';
import { runCompletionGate } from './completion-gate.js';
import { executeHooks } from './hooks.js';
import { recordIteration, recordToolCall, recordCompletionGate, startSessionMetrics, endSessionMetrics } from './agent-metrics.js';
import { recordIteration, recordToolCall, startSessionMetrics, endSessionMetrics } from './agent-metrics.js';
import { getEffectiveNumCtx } from '../components/model-bar.js';
import type {
OllamaMessage,
@@ -185,58 +177,12 @@ const SIDE_EFFECT_TOOLS = new Set([
]);
// ═══════════════════════════════════════════════════════════════
// 跨轮次死循环检测
// 追踪每轮工具调用签名,检测连续重复以防止模型陷入死循环
// 与 R104(仅记录日志)不同,本机制在达到阈值时主动干预:
// - 连续 3 轮相同 → 注入事实性提示(不修改模型输出)
// - 注入提示后仍连续 5 轮相同 → 硬性熔断,强制终止
// 跨轮次死循环检测已删除 — 硬熔断过于激进,软提示干扰 AI 判断
// AI 应自行判断是否需要继续调用相同工具
// ═══════════════════════════════════════════════════════════════
const _loopToolSignatures: string[] = [];
const LOOP_HISTORY_MAX = 10;
let _deadlockHintInjected = false;
// P1-E6 修复:待清理的记忆提取 timer 列表,finally 块统一清理
const _pendingMemoryTimers: ReturnType<typeof setTimeout>[] = [];
/** 记录本轮工具调用签名(每轮 handleThinking 成功后调用一次) */
function recordLoopSignature(toolCalls: ToolCall[]): void {
if (toolCalls.length === 0) {
_loopToolSignatures.push('__no_tools__');
_deadlockHintInjected = false; // 无工具调用,模型已改变行为,重置提示标志
return;
}
const sig = toolCalls.map(tc =>
`${tc.function.name}:${JSON.stringify(tc.function.arguments, Object.keys(tc.function.arguments || {}).sort())}`
).join('|');
// 签名变化时重置提示标志(模型已改变行为)
if (_loopToolSignatures.length > 0 && _loopToolSignatures[_loopToolSignatures.length - 1] !== sig) {
_deadlockHintInjected = false;
}
_loopToolSignatures.push(sig);
if (_loopToolSignatures.length > LOOP_HISTORY_MAX) {
_loopToolSignatures.shift();
}
}
/** 检测连续 N 轮相同的工具调用签名 */
function detectLoopDeadlock(minCount: number): { detected: boolean; toolName: string; count: number } {
if (_loopToolSignatures.length < minCount) return { detected: false, toolName: '', count: 0 };
const recent = _loopToolSignatures.slice(-minCount);
if (recent[0] === '__no_tools__') return { detected: false, toolName: '', count: 0 };
const allSame = recent.every(s => s === recent[0]);
if (allSame) {
const firstSig = recent[0];
const toolName = firstSig.split(':')[0] || 'unknown';
return { detected: true, toolName, count: minCount };
}
return { detected: false, toolName: '', count: 0 };
}
/** 重置死循环检测状态(新会话开始时调用) */
function resetLoopDeadlockDetector(): void {
_loopToolSignatures.length = 0;
_deadlockHintInjected = false;
}
/**
* R3: 工具执行超时配置
* Agent Loop
@@ -1214,8 +1160,6 @@ function snapshotLoopContext(ctx: LoopContext): void {
// P0-P3 修复:补全关键字段,至少保证运行时调试可见
mode: ctx.mode,
emergencyCompressCount: ctx.emergencyCompressCount,
completionGateFailCount: ctx.completionGateFailCount,
verifyWarnings: ctx.verifyWarnings,
compressedThisCycle: ctx.compressedThisCycle,
messageCount: ctx.messages.length,
});
@@ -1306,11 +1250,9 @@ async function handleInit(
): Promise<void> {
// 新一轮对话,清空工具缓存
toolResultCache.clear();
resetAllSafetyState(); // R51-R56: 重置所有安全状态
resetLoopDeadlockDetector(); // 重置跨轮次死循环检测
resetTokenBudget(); // R93: 重置 Token 预算追踪
setTokenBudgetNumCtx(getEffectiveNumCtx()); // R93: 设置当前预算 numCtx
setUserGoal(userContent); // R56: 设置用户原始目标
resetAllSafetyState(); // R51-R56: 重置所有安全状态
resetTokenBudget(); // R93: 重置 Token 预算追踪
setTokenBudgetNumCtx(getEffectiveNumCtx()); // R93: 设置当前预算 numCtx
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
// R55: 语义工具检索 — 根据用户查询过滤相关工具,减少 token 占用
@@ -1520,134 +1462,8 @@ function extractPlanSteps(content: string): string[] {
return steps.slice(0, 8);
}
/**
*
* /
*/
const TOOLS_NEED_VERIFY = new Set([
'write_file', 'edit_file', 'delete_file', 'create_directory',
'move_file', 'copy_file', 'download_file',
]);
async function verifyToolResult(
toolName: string,
args: Record<string, unknown>,
result: Record<string, unknown>,
): Promise<string | null> {
// P3 #13 修复:返回警告字符串供 Completion Gate 使用
if (!TOOLS_NEED_VERIFY.has(toolName) || !result.success) return null;
const bridge = window.metonaDesktop;
if (!bridge?.isDesktop) return null;
try {
switch (toolName) {
case 'write_file': {
const path = String(args.path || '');
const expected = String(args.content || '');
const read = await bridge.tool.execute('read_file', { path, mode: 'text' });
if (read.success) {
const actual = String(read.content || '');
if (actual.length === 0) {
logWarn(`核验 write_file: ${path} 内容为空`);
return `write_file 核验: ${path} 写入后内容为空`;
} else if (Math.abs(actual.length - expected.length) > expected.length * 0.5) {
logWarn(`核验 write_file: ${path} 期望 ${expected.length}B 实际 ${actual.length}B`);
return `write_file 核验: ${path} 写入内容大小不符 (期望 ${expected.length}B, 实际 ${actual.length}B)`;
} else {
logInfo(`核验 write_file ✅: ${path} (${actual.length}B)`);
}
}
break;
}
case 'edit_file': {
const path = String(args.path || '');
const newText = String(args.new_text || '');
const read = await bridge.tool.execute('read_file', { path, mode: 'text' });
if (read.success) {
const actual = String(read.content || '');
if (!actual.includes(newText)) {
logWarn(`核验 edit_file: ${path} 不包含期望的新内容`);
return `edit_file 核验: ${path} 编辑后文件不包含期望的新内容`;
} else {
logInfo(`核验 edit_file ✅: ${path}`);
}
}
break;
}
case 'delete_file': {
// 支持批量删除核验:检查 paths 数组和单个 path
const pathsToCheck: string[] = [];
if (args.paths && Array.isArray(args.paths)) {
pathsToCheck.push(...args.paths.map((p: unknown) => String(p)));
}
if (args.path) pathsToCheck.push(String(args.path));
for (const p of pathsToCheck) {
const info = await bridge.tool.execute('get_file_info', { path: p });
if (info.success) {
logWarn(`核验 delete_file: ${p} 仍然存在,删除未生效`);
return `delete_file 核验: ${p} 删除后仍然存在`;
} else {
logInfo(`核验 delete_file ✅: ${p} 已不存在`);
}
}
break;
}
case 'create_directory': {
const path = String(args.path || '');
const info = await bridge.tool.execute('get_file_info', { path });
if (info.success && (info as any).type === 'directory') {
logInfo(`核验 create_directory ✅: ${path}`);
} else {
logWarn(`核验 create_directory: ${path} 未成功创建`);
return `create_directory 核验: ${path} 目录未成功创建`;
}
break;
}
case 'move_file': {
const src = String(args.source || '');
const dest = String(args.destination || '');
const [srcInfo, destInfo] = await Promise.all([
bridge.tool.execute('get_file_info', { path: src }),
bridge.tool.execute('get_file_info', { path: dest }),
]);
if (srcInfo.success) {
logWarn(`核验 move_file: 源文件 ${src} 仍然存在`);
return `move_file 核验: 源文件 ${src} 移动后仍然存在`;
} else if (destInfo.success) {
logInfo(`核验 move_file ✅: ${src}${dest}`);
} else {
logWarn(`核验 move_file: ${src}${dest} 源和目标均不存在`);
return `move_file 核验: ${src}${dest} 移动后源和目标均不存在`;
}
break;
}
case 'copy_file': {
const dest = String(args.destination || '');
const destInfo = await bridge.tool.execute('get_file_info', { path: dest });
if (destInfo.success) {
logInfo(`核验 copy_file ✅: ${dest} (${(destInfo as any).size}B)`);
} else {
logWarn(`核验 copy_file: 目标 ${dest} 不存在`);
return `copy_file 核验: 目标 ${dest} 复制后不存在`;
}
break;
}
case 'download_file': {
const dest = String(args.destination || '');
const info = await bridge.tool.execute('get_file_info', { path: dest });
if (info.success && (info as any).size > 0) {
logInfo(`核验 download_file ✅: ${dest} (${(info as any).size}B)`);
} else {
logWarn(`核验 download_file: ${dest} 不存在或为空`);
return `download_file 核验: ${dest} 下载后不存在或为空`;
}
break;
}
}
} catch { /* 核验失败不阻塞主流程 */ }
return null;
}
// verifyToolResult 已删除 — 工具结果核验过于粗糙,误报边界情况
// AI 应基于工具返回的 success/error 字段自行判断结果有效性
/**
* THINKING LLM
@@ -1668,33 +1484,6 @@ async function handleThinking(
ctx.thinking = '';
ctx.toolCalls.length = 0;
// ── 跨轮次死循环检测 ──
// 硬性熔断:已注入提示后仍连续 5 轮相同 → 强制终止
if (_deadlockHintInjected) {
const hardBreak = detectLoopDeadlock(5);
if (hardBreak.detected) {
logWarn(`死循环熔断: 连续 ${hardBreak.count} 轮相同工具调用 (${hardBreak.toolName}),已注入提示但仍未改善,强制终止`);
const fallbackReply = `已执行工具调用但模型未能生成最终回复。最后执行的工具: ${hardBreak.toolName}。请尝试重新描述任务或切换模型。`;
ctx.content = fallbackReply;
callbacks.onDone(fallbackReply, ctx.allToolRecords.length > 0 ? ctx.allToolRecords : undefined, makeStats(ctx));
transition(ctx, S.TERMINATED);
return;
}
}
// 软性提示:连续 3 轮相同工具调用 → 注入事实性提示(不修改模型输出)
if (!_deadlockHintInjected) {
const softWarn = detectLoopDeadlock(3);
if (softWarn.detected) {
logWarn(`死循环提示: 连续 ${softWarn.count} 轮相同工具调用 (${softWarn.toolName}),注入事实性提示`);
ctx.messages.push({
role: 'user',
content: `[系统提示] 你已连续 ${softWarn.count} 轮调用相同的工具 (${softWarn.toolName}) 并获得相同结果。任务可能已完成,请基于已有工具结果直接回复用户,不要再调用工具。`,
ephemeral: true,
});
_deadlockHintInjected = true;
}
}
// ── Plan Mode 执行进度注入 — 仅在进度变化时注入,避免每轮重复 ──
if (ctx.mode === 'plan' && ctx.loopCount >= 1) {
const tracker = getPlanTracker();
@@ -1970,9 +1759,6 @@ async function handleThinking(
ctx.loopPromptEvalCount = 0;
ctx.loopInferenceNs = 0;
// 记录本轮工具调用签名(用于跨轮次死循环检测)
recordLoopSignature(ctx.toolCalls);
transition(ctx, S.PARSING);
}
@@ -2108,29 +1894,8 @@ async function handleExecuting(
}, null];
}
// R77: 工具调用速率限制 — 防止快速连续调用同一工具浪费资源
const rateLimitResult = checkRateLimit(call.function.name);
if (!rateLimitResult.allowed) {
const waitSec = Math.ceil(rateLimitResult.retryAfterMs / 1000);
logWarn(`R77: 速率限制拦截: ${call.function.name},需等待 ${waitSec}s`);
return [{
name: call.function.name, arguments: call.function.arguments,
result: { success: false, error: `工具「${call.function.name}」调用频率过高,请等待 ${waitSec} 秒后重试。` },
status: 'error' as const, timestamp: Date.now()
}, null];
}
// R87: 工具熔断器检查 — 连续失败后自动禁用工具
const circuitBreaker = isToolCircuitBroken(call.function.name);
if (circuitBreaker.broken) {
const waitSec = Math.ceil(circuitBreaker.remainingMs / 1000);
logWarn(`R87: 熔断器拦截: ${call.function.name},冷却中(还需 ${waitSec}s`);
return [{
name: call.function.name, arguments: call.function.arguments,
result: { success: false, error: `工具「${call.function.name}」因连续失败已被暂时禁用,请等待 ${waitSec} 秒后重试,或使用其他方法。` },
status: 'error' as const, timestamp: Date.now()
}, null];
}
// R77/R87 已删除:速率限制 + 熔断器 — 剥夺 AI 试错空间,误伤密集型任务
// AI 应基于工具返回的错误信息自行调整策略
// R79: 路径沙箱验证 — 确保文件操作不超出工作空间边界
const FILE_PATH_TOOLS = new Set([
@@ -2221,12 +1986,6 @@ async function handleExecuting(
state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal)
.catch(err => ({ success: false, error: err?.message || String(err) }) as ToolResult);
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
// R87: 记录工具成功/失败到熔断器
if (result.success) {
recordToolSuccess(call.function.name);
} else {
recordToolFailure(call.function.name);
}
// 记录 write_file 成功路径及内容指纹,供 FileWriteDedup Hook 做内容级去重
if (call.function.name === 'write_file' && result.success && call.function.arguments?.path) {
const { addWrittenFile } = await import('./hooks.js');
@@ -2310,13 +2069,6 @@ async function handleExecuting(
ctx.allToolRecords.push(record);
// R88: 为工具结果添加元数据提示(token 估算)
const formattedResult = addResultMetadata(formatToolResultForModel(record.name, record.result!));
// R104: 检测重复工具结果(仅记录日志,不修改结果内容)
if (record.status === 'success') {
const dedup = isDuplicateToolResult(record.name, formattedResult);
if (dedup.duplicate) {
logInfo(`R104: 检测到重复工具结果: ${record.name},已记录`);
}
}
// R92: 工具结果格式标准化 — 添加统一头信息(工具名、状态、耗时、结果大小)
const resultDuration = Date.now() - record.timestamp;
const resultSize = formattedResult.length;
@@ -2333,15 +2085,6 @@ async function handleExecuting(
if (cacheKey) setToolCache(cacheKey, { result: record.result!, timestamp: Date.now() });
// 记录度量
recordToolCall(record.name, record.status, Date.now() - record.timestamp);
// ── 通用工具结果核验 — 所有写类工具执行后验证 ──
// P3 #13 修复:收集核验警告到 ctx.verifyWarnings,供 Completion Gate 使用
if (record.status === 'success') {
const warning = await verifyToolResult(record.name, record.arguments, record.result!);
if (warning) {
ctx.verifyWarnings.push(warning);
logWarn(warning);
}
}
// ── post_tool Hook ──
executeHooks('post_tool', ctx, { toolName: record.name, toolArgs: record.arguments, toolResult: record.result! }).catch(err => logWarn('post_tool hook 异常', String(err)));
// P0 修复: 使用参数匹配而非仅工具名匹配,避免同批次同名工具的错误关联
@@ -2695,35 +2438,9 @@ async function handleReflecting(
}
} catch { /* Hook 异常不影响主流程 */ }
try {
const gateResult = await runCompletionGate(ctx);
if (!gateResult.passed) {
recordCompletionGate(false);
logWarn(`Completion Gate: ${gateResult.reason}`);
// P1 #5 修复:Gate 失败时注入纠正提示并重试(最多 1 次),而非仅记录日志
const MAX_GATE_RETRIES = 1;
if (ctx.completionGateFailCount < MAX_GATE_RETRIES && ctx.loopCount < ctx.maxLoops) {
ctx.completionGateFailCount++;
logInfo(`Completion Gate: 注入纠正提示,重试 (${ctx.completionGateFailCount}/${MAX_GATE_RETRIES})`);
ctx.messages.push({
role: 'user',
content: `[系统提示] ${gateResult.reason}\n请基于已有信息补充完整你的回答,不要重复之前的工具调用。`,
ephemeral: true,
});
transition(ctx, S.THINKING);
return;
} else {
logWarn(`Completion Gate: 已达重试上限或循环上限,接受当前结果`);
}
} else {
recordCompletionGate(true);
}
// R90: 记录完成门控评分到日志
if (gateResult.score < 100) {
logInfo(`R90: Completion Gate 评分: ${gateResult.score}/100`);
}
} catch { /* Gate 异常不影响主流程 */ }
// Completion Gate 已删除 — 所有检查项(notThinking/toolResultReview/contextEfficiency/planModeCompletion)均已删除
// AI 应基于工具结果和上下文自行判断是否需要继续或完成
// Plan Mode 下 AI 可通过 formatPlanStatus() 看到剩余步骤,应自行决定是否继续
logInfo('模型停止工具调用 → ReAct Agent Loop 结束');
// ── 保存 Plan Mode 最终进度到状态,供下一轮 Loop 注入历史 ──
@@ -2881,8 +2598,6 @@ export async function runAgentLoop(
startTime: Date.now(),
lastLoopStats: undefined,
emergencyCompressCount: 0,
completionGateFailCount: 0,
verifyWarnings: [],
compressedThisCycle: false,
};
-17
View File
@@ -37,7 +37,6 @@ interface SessionMetrics {
toolCalls: Array<{ name: string; status: string; duration?: number }>;
totalInputTokens: number;
totalOutputTokens: number;
completionGatePassed: boolean;
errorPatterns: string[];
}
@@ -55,7 +54,6 @@ export function startSessionMetrics(sessionId: string, model: string): void {
toolCalls: [],
totalInputTokens: 0,
totalOutputTokens: 0,
completionGatePassed: false,
errorPatterns: [],
};
logDebug(`Agent Metrics: 开始采集会话 ${sessionId}`);
@@ -82,12 +80,6 @@ export function recordToolCall(name: string, status: string, durationMs?: number
}
}
/** 完成门控结果 */
export function recordCompletionGate(passed: boolean): void {
if (!currentMetrics) return;
currentMetrics.completionGatePassed = passed;
}
/** 结束会话度量 */
export function endSessionMetrics(): SessionMetrics | null {
if (!currentMetrics) return null;
@@ -124,7 +116,6 @@ function persistMetricsHistory(): void {
iterations: m.totalIterations,
toolCallCount: m.toolCalls.length,
toolSuccessRate: getToolSuccessRate(m),
completionGatePassed: m.completionGatePassed,
errorPatterns: m.errorPatterns,
inputTokens: m.totalInputTokens,
outputTokens: m.totalOutputTokens,
@@ -152,7 +143,6 @@ export function loadMetricsHistory(): void {
toolCalls: [],
totalInputTokens: item.inputTokens || 0,
totalOutputTokens: item.outputTokens || 0,
completionGatePassed: item.completionGatePassed || false,
errorPatterns: item.errorPatterns || [],
});
}
@@ -192,7 +182,6 @@ export function aggregateMetrics(): AgentMetrics {
totalSessions: 0,
avgIterationsPerTask: 0,
toolSuccessRate: 0,
avgCompletionScore: 0,
frequentErrors: [],
tokenEfficiency: 0,
collectedAt: Date.now(),
@@ -221,7 +210,6 @@ export function aggregateMetrics(): AgentMetrics {
totalSessions: history.length,
avgIterationsPerTask: Math.round(totalIterations / history.length),
toolSuccessRate: allToolCalls.length > 0 ? successCalls / allToolCalls.length : 0,
avgCompletionScore: history.filter(m => m.completionGatePassed).length / history.length,
frequentErrors,
tokenEfficiency: totalInputTokens > 0 ? totalOutputTokens / totalInputTokens : 0,
collectedAt: Date.now(),
@@ -330,7 +318,6 @@ export function formatMetricsReport(metrics: AgentMetrics): string {
`总会话数: ${metrics.totalSessions}`,
`平均迭代/任务: ${metrics.avgIterationsPerTask}`,
`工具成功率: ${formatPercent(metrics.toolSuccessRate)}`,
`完成门控通过率: ${formatPercent(metrics.avgCompletionScore)}`,
`Token 效率: ${formatPercent(metrics.tokenEfficiency)}`,
`高频错误: ${metrics.frequentErrors.length > 0 ? metrics.frequentErrors.map(e => `${e.pattern}(${e.count}次)`).join(', ') : '无'}`,
].join('\n');
@@ -347,7 +334,6 @@ export function exportMetricsJSON(): string {
sessions_total: metrics.totalSessions,
avg_iterations_per_task: metrics.avgIterationsPerTask,
tool_success_rate: parseFloat(metrics.toolSuccessRate.toFixed(4)),
completion_gate_pass_rate: parseFloat(metrics.avgCompletionScore.toFixed(4)),
token_efficiency: parseFloat(metrics.tokenEfficiency.toFixed(4)),
top_errors: metrics.frequentErrors.slice(0, 5),
improvement_suggestions: generateImprovementSuggestions().slice(0, 3).map(s => ({
@@ -373,9 +359,6 @@ export function exportMetricsPrometheus(): string {
lines.push('# HELP metona_tool_success_rate Tool call success rate (0-1)');
lines.push('# TYPE metona_tool_success_rate gauge');
lines.push(`metona_tool_success_rate ${metrics.toolSuccessRate.toFixed(4)}`);
lines.push('# HELP metona_completion_gate_pass_rate Completion gate pass rate (0-1)');
lines.push('# TYPE metona_completion_gate_pass_rate gauge');
lines.push(`metona_completion_gate_pass_rate ${metrics.avgCompletionScore.toFixed(4)}`);
lines.push('# HELP metona_token_efficiency Token output/input efficiency ratio');
lines.push('# TYPE metona_token_efficiency gauge');
lines.push(`metona_token_efficiency ${metrics.tokenEfficiency.toFixed(4)}`);
+12 -446
View File
@@ -97,227 +97,10 @@ export function detectConsecutiveIdentical(minCount: number): { detected: boolea
}
// ═══════════════════════════════════════════════════════════════
// R56: 目标对齐验证
// R56 已删除:目标对齐验证 — 关键词匹配粗糙,反复注入干扰 AI 判断
// R63 已删除:工具调用速率限制 — 剥夺 AI 试错空间,误伤密集型任务
// ═══════════════════════════════════════════════════════════════
/** 用户原始目标(在 handleInit 时设置) */
let _userGoal: string = '';
/** 设置用户原始目标 */
export function setUserGoal(goal: string): void {
_userGoal = goal.slice(0, 500); // 限制长度防止占用过多内存
}
/** 获取用户原始目标 */
export function getUserGoal(): string {
return _userGoal;
}
/** 重置用户目标 */
export function resetUserGoal(): void {
_userGoal = '';
}
/** 目标对齐检测结果 */
export interface GoalAlignmentResult {
aligned: boolean;
reason: string;
suggestion?: string;
}
/**
* R56: 检测 Agent
*
*
* 1.
* 2. 5
* 3.
*
* @param recentToolNames N
* @param recentContent N
* @param loopCount
*/
export function checkGoalAlignment(
recentToolNames: string[],
recentContent: string,
loopCount: number,
): GoalAlignmentResult {
if (!_userGoal || _userGoal.length < 10) {
return { aligned: true, reason: '目标太短,跳过对齐检测' };
}
// 仅在循环数 >= 6 时开始检测(前几轮可能是探索阶段)
if (loopCount < 6) {
return { aligned: true, reason: '前期探索阶段,跳过检测' };
}
const goalLower = _userGoal.toLowerCase();
// 提取目标关键词
const goalKeywords = extractGoalKeywords(_userGoal);
// 检查最近内容是否包含目标关键词
const contentLower = recentContent.toLowerCase();
const contentMatches = goalKeywords.filter(kw => contentLower.includes(kw.toLowerCase()));
// 检查最近的工具调用是否与目标相关
// 每种工具与特定目标类型的关联度
const toolGoalRelevance = assessToolGoalRelevance(recentToolNames, goalLower);
// 如果最近内容中包含目标关键词,认为是对齐的
if (contentMatches.length >= 1) {
return { aligned: true, reason: `内容中包含目标关键词: ${contentMatches.join(', ')}` };
}
// 如果工具调用与目标高度相关,认为是对齐的
if (toolGoalRelevance.relevant) {
return { aligned: true, reason: toolGoalRelevance.reason };
}
// 偏离判定:内容无关键词 + 工具不相关 + 已过探索阶段
return {
aligned: false,
reason: `最近 ${recentToolNames.length} 轮工具调用 (${recentToolNames.slice(-3).join(', ')}) 与用户目标"${_userGoal.slice(0, 60)}..."可能不相关`,
suggestion: `请回顾用户原始任务: "${_userGoal.slice(0, 100)}"。如果当前操作是在为任务做准备,请继续;如果已经偏离,请调整方向。`,
};
}
/** 从用户目标中提取关键词 */
function extractGoalKeywords(goal: string): string[] {
const keywords: string[] = [];
// 中文关键词
const cnPatterns = [
/搜索|查找|查一下|搜一下|检索|查询/g,
/写入|创建|生成|保存|输出/g,
/运行|执行|编译|构建/g,
/读取|查看|打开/g,
/修改|编辑|更新|替换/g,
/删除|移除|清理/g,
/下载|安装/g,
/分析|研究|评估|对比/g,
/测试|验证|检查/g,
/部署|发布/g,
];
// 英文关键词
const enPatterns = [
/\b(search|find|lookup)\b/gi,
/\b(write|create|generate|save)\b/gi,
/\b(run|execute|build|compile)\b/gi,
/\b(read|view|open)\b/gi,
/\b(edit|modify|update|replace)\b/gi,
/\b(delete|remove|clean)\b/gi,
/\b(download|install)\b/gi,
/\b(analyze|research|evaluate|compare)\b/gi,
/\b(test|verify|check)\b/gi,
/\b(deploy|publish)\b/gi,
];
for (const p of [...cnPatterns, ...enPatterns]) {
const matches = goal.match(p);
if (matches) keywords.push(...matches);
}
// 提取路径/文件名(如果目标中包含)
const pathMatches = goal.match(/[\w-]+\.(ts|js|py|json|md|txt|html|css|go|rs|java|cpp|c)/gi);
if (pathMatches) keywords.push(...pathMatches);
// 提取引号中的内容
const quotedMatches = goal.match(/[""']([^""']{3,30})[""']/g);
if (quotedMatches) {
for (const q of quotedMatches) {
keywords.push(q.replace(/[""']/g, ''));
}
}
return [...new Set(keywords)];
}
/** 评估工具调用与用户目标的相关性 */
function assessToolGoalRelevance(toolNames: string[], goalLower: string): { relevant: boolean; reason: string } {
if (toolNames.length === 0) return { relevant: true, reason: '无工具调用,跳过' };
// 文件操作类目标
const isFileTask = /文件|目录|file|directory|path|路径/.test(goalLower);
// 搜索类目标
const isSearchTask = /搜索|查找|查|search|find|grep/.test(goalLower);
// 命令执行类目标
const isCommandTask = /运行|执行|命令|终端|run|execute|command|shell/.test(goalLower);
// 网络类目标
const isWebTask = /网页|网站|url|web|fetch|搜索|search/.test(goalLower);
// Git 类目标
const isGitTask = /git|提交|推送|分支|commit|push|pull/.test(goalLower);
// 浏览器类目标
const isBrowserTask = /浏览器|网页|截图|browser|screenshot/.test(goalLower);
const recentTools = toolNames.slice(-5);
if (isFileTask && recentTools.some(t => /read_file|write_file|edit_file|list_directory|search_files|tree/.test(t))) {
return { relevant: true, reason: '文件操作与文件类目标相关' };
}
if (isSearchTask && recentTools.some(t => /search_files|web_search|grep/.test(t))) {
return { relevant: true, reason: '搜索操作与搜索类目标相关' };
}
if (isCommandTask && recentTools.some(t => /run_command/.test(t))) {
return { relevant: true, reason: '命令执行与命令类目标相关' };
}
if (isWebTask && recentTools.some(t => /web_search|web_fetch|browser_open/.test(t))) {
return { relevant: true, reason: '网络操作与网络类目标相关' };
}
if (isGitTask && recentTools.some(t => /git/.test(t))) {
return { relevant: true, reason: 'Git 操作与 Git 类目标相关' };
}
if (isBrowserTask && recentTools.some(t => /browser_/.test(t))) {
return { relevant: true, reason: '浏览器操作与浏览器类目标相关' };
}
// memory 和 plan_track 始终认为相关
if (recentTools.some(t => /memory|plan_track/.test(t))) {
return { relevant: true, reason: '记忆/计划操作始终相关' };
}
return { relevant: false, reason: '最近工具调用与目标类型不匹配' };
}
// ═══════════════════════════════════════════════════════════════
// R63: 工具调用速率限制 — 防止快速连续调用同一工具浪费资源
// ═══════════════════════════════════════════════════════════════
const _toolCallTimestamps = new Map<string, number[]>();
const RATE_LIMIT_WINDOW = 60_000; // 60 秒窗口
const RATE_LIMIT_DEFAULT = 20; // 默认每分钟最多 20 次
/** R63: 工具特定的速率限制配置 */
const TOOL_RATE_LIMITS: Record<string, number> = {
web_search: 10, // 每分钟最多 10 次搜索
web_fetch: 8, // 每分钟最多 8 次抓取
run_command: 15, // 每分钟最多 15 次命令
browser_open: 8, // 每分钟最多 8 次浏览器打开
download_file: 5, // 每分钟最多 5 次下载
};
/** R63: 检查工具调用是否超过速率限制 */
export function checkRateLimit(toolName: string): { allowed: boolean; retryAfterMs: number } {
const limit = TOOL_RATE_LIMITS[toolName] ?? RATE_LIMIT_DEFAULT;
const now = Date.now();
const timestamps = _toolCallTimestamps.get(toolName) || [];
// 清理过期时间戳
const valid = timestamps.filter(t => now - t < RATE_LIMIT_WINDOW);
if (valid.length >= limit) {
// 计算最早过期时间
const oldest = valid[0];
const retryAfterMs = RATE_LIMIT_WINDOW - (now - oldest);
return { allowed: false, retryAfterMs: Math.max(1000, retryAfterMs) };
}
valid.push(now);
_toolCallTimestamps.set(toolName, valid);
return { allowed: true, retryAfterMs: 0 };
}
// ═══════════════════════════════════════════════════════════════
// R66-R67: 错误分类系统 — 区分瞬态/永久错误,指导重试策略
// ═══════════════════════════════════════════════════════════════
@@ -451,7 +234,12 @@ export function validatePathSandbox(
}
// 检查路径遍历攻击
const relativePath = normalized.toLowerCase().replace(normalizedWs.toLowerCase(), '').replace(/^[\\/]/, '');
// P0 修复:用前缀锚定 replace,避免工作空间路径是另一路径子串时误判
const wsLower = normalizedWs.toLowerCase();
const normLower = normalized.toLowerCase();
const relativePath = normLower.startsWith(wsLower)
? normLower.slice(wsLower.length).replace(/^[\\/]/, '')
: normLower;
if (relativePath.includes('..')) {
// 允许在工作空间内部的相对路径引用
const segments = relativePath.split(/[\\/]/);
@@ -485,45 +273,9 @@ function isAbsolute(p: string): boolean {
}
// ═══════════════════════════════════════════════════════════════
// R87: 工具熔断器 — 连续失败后自动禁用工具,防止雪崩
// R87 已删除:工具熔断器 — 剥夺 AI 试错空间,连续失败可能是参数调试过程
// ═══════════════════════════════════════════════════════════════
/** 工具失败计数器 */
const _toolFailureCount = new Map<string, number>();
const CIRCUIT_BREAKER_THRESHOLD = 5; // 连续失败 5 次后熔断
const CIRCUIT_BREAKER_COOLDOWN = 60_000; // 熔断冷却 60 秒
const _circuitBreakerTripped = new Map<string, number>(); // toolName → trip timestamp
/** R87: 记录工具失败 */
export function recordToolFailure(toolName: string): void {
const count = (_toolFailureCount.get(toolName) || 0) + 1;
_toolFailureCount.set(toolName, count);
if (count >= CIRCUIT_BREAKER_THRESHOLD) {
_circuitBreakerTripped.set(toolName, Date.now());
logWarn(`R87: 熔断器触发 — ${toolName} 连续失败 ${count} 次,禁用 60 秒`);
}
}
/** R87: 记录工具成功(重置失败计数) */
export function recordToolSuccess(toolName: string): void {
_toolFailureCount.delete(toolName);
_circuitBreakerTripped.delete(toolName);
}
/** R87: 检查工具是否被熔断 */
export function isToolCircuitBroken(toolName: string): { broken: boolean; remainingMs: number } {
const tripTime = _circuitBreakerTripped.get(toolName);
if (!tripTime) return { broken: false, remainingMs: 0 };
const elapsed = Date.now() - tripTime;
if (elapsed >= CIRCUIT_BREAKER_COOLDOWN) {
// 冷却期过了,半开状态:允许调用但保持失败计数
_circuitBreakerTripped.delete(toolName);
_toolFailureCount.set(toolName, CIRCUIT_BREAKER_THRESHOLD - 1); // 只给一次机会
return { broken: false, remainingMs: 0 };
}
return { broken: true, remainingMs: CIRCUIT_BREAKER_COOLDOWN - elapsed };
}
// ═══════════════════════════════════════════════════════════════
// R88: 工具结果元数据 — 为模型提供结果大小的上下文提示
// ═══════════════════════════════════════════════════════════════
@@ -621,56 +373,9 @@ export function recordErrorPattern(toolName: string, errorMsg: string): string |
}
// ═══════════════════════════════════════════════════════════════
// R104: 工具结果去重 — 相似结果检测,减少上下文中的冗余
// R104 已删除:工具结果去重 — 误伤轮询类任务(如反复 run_command 检查构建状态)
// ═══════════════════════════════════════════════════════════════
export interface ToolResultFingerprint {
toolName: string;
contentHash: string;
timestamp: number;
resultLength: number;
}
const _resultFingerprints: ToolResultFingerprint[] = [];
const MAX_FINGERPRINTS = 50;
/** R104: 计算内容的简化指纹(用于相似度检测) */
function computeResultFingerprint(content: string): string {
// 取内容的前200字符 + 后100字符作为指纹
const head = content.slice(0, 200);
const tail = content.slice(-100);
let hash = 5381;
const combined = head + tail;
for (let i = 0; i < combined.length; i++) {
hash = ((hash << 5) + hash + combined.charCodeAt(i)) & 0x7fffffff;
}
return hash.toString(36);
}
/** R104: 检查工具结果是否与最近的结果高度相似 */
export function isDuplicateToolResult(toolName: string, resultContent: string): { duplicate: boolean; similarTo?: string } {
const hash = computeResultFingerprint(resultContent);
const now = Date.now();
const DEDUP_WINDOW = 60_000; // 60 秒内的结果视为候选
// 查找相同工具+相同指纹的结果
for (const fp of _resultFingerprints) {
if (fp.toolName === toolName && fp.contentHash === hash && (now - fp.timestamp) < DEDUP_WINDOW) {
return { duplicate: true, similarTo: `${fp.timestamp}` };
}
}
// 记录当前结果
_resultFingerprints.push({
toolName, contentHash: hash, timestamp: now, resultLength: resultContent.length,
});
if (_resultFingerprints.length > MAX_FINGERPRINTS) {
_resultFingerprints.shift();
}
return { duplicate: false };
}
// ═══════════════════════════════════════════════════════════════
// R109: 工具参数消毒 — 防止通过工具参数注入恶意内容
// ═══════════════════════════════════════════════════════════════
@@ -718,55 +423,25 @@ export interface AgentDiagnostics {
timestamp: number;
toolResultStoreSize: number;
toolCallHistoryLength: number;
rateLimitActiveTools: string[];
circuitBreakerStatus: Array<{ tool: string; broken: boolean; failures: number }>;
errorPatternCount: number;
topErrorPatterns: Array<{ tool: string; signature: string; count: number }>;
resultFingerprintCount: number;
duplicateResultCount: number;
userGoal: string;
}
/** R112: 收集 Agent 诊断信息 */
export function collectDiagnostics(): AgentDiagnostics {
const rateLimitActive: string[] = [];
const now = Date.now();
for (const [tool, timestamps] of _toolCallTimestamps) {
const valid = timestamps.filter(t => now - t < RATE_LIMIT_WINDOW);
if (valid.length > 0) {
rateLimitActive.push(`${tool}(${valid.length}/${TOOL_RATE_LIMITS[tool] ?? RATE_LIMIT_DEFAULT})`);
}
}
const circuitBreakerStatus = Array.from(_toolFailureCount.entries()).map(([tool, count]) => ({
tool,
broken: _circuitBreakerTripped.has(tool),
failures: count,
}));
const topErrorPatterns = Array.from(_errorPatterns.values())
.sort((a, b) => b.count - a.count)
.slice(0, 5)
.map(e => ({ tool: e.toolName, signature: e.errorSignature, count: e.count }));
const duplicateCount = _resultFingerprints.filter(fp => {
// 同一工具同一指纹出现多次
return _resultFingerprints.some(other =>
other !== fp && other.toolName === fp.toolName && other.contentHash === fp.contentHash
);
}).length;
return {
timestamp: now,
toolResultStoreSize: _toolResultStore.size,
toolCallHistoryLength: _toolCallHistory.length,
rateLimitActiveTools: rateLimitActive,
circuitBreakerStatus,
errorPatternCount: _errorPatterns.size,
topErrorPatterns,
resultFingerprintCount: _resultFingerprints.length,
duplicateResultCount: duplicateCount,
userGoal: _userGoal.slice(0, 80),
};
}
@@ -777,18 +452,11 @@ export function formatDiagnosticsReport(diag: AgentDiagnostics): string {
`${'─'.repeat(50)}`,
`Tool Result Store: ${diag.toolResultStoreSize} entries`,
`Tool Call History: ${diag.toolCallHistoryLength} entries`,
`Rate Limit Active: ${diag.rateLimitActiveTools.length > 0 ? diag.rateLimitActiveTools.join(', ') : 'none'}`,
`Circuit Breakers:`,
`Error Patterns: ${diag.errorPatternCount}`,
];
for (const cb of diag.circuitBreakerStatus) {
lines.push(` ${cb.broken ? '🔴' : '🟢'} ${cb.tool}: ${cb.failures} failures${cb.broken ? ' (BROKEN)' : ''}`);
}
lines.push(`Error Patterns: ${diag.errorPatternCount}`);
for (const ep of diag.topErrorPatterns) {
lines.push(` ⚠️ ${ep.tool}: "${ep.signature}" (${ep.count}x)`);
}
lines.push(`Result Fingerprints: ${diag.resultFingerprintCount} (duplicates: ${diag.duplicateResultCount})`);
lines.push(`User Goal: "${diag.userGoal}"`);
return lines.join('\n');
}
@@ -1007,14 +675,8 @@ export function checkArchivedReferences(messages: Array<{ content?: string }>):
export function resetAllSafetyState(): void {
_toolResultStore.clear();
_toolCallHistory.length = 0;
_userGoal = '';
_toolCallTimestamps.clear();
_toolFailureCount.clear();
_circuitBreakerTripped.clear();
// R97: 清理错误模式
_errorPatterns.clear();
// R104: 清理结果指纹
_resultFingerprints.length = 0;
// R118: 清理性能分析数据
_loopTimings.length = 0;
logInfo('Agent Safety: 所有安全状态已重置');
@@ -1027,12 +689,7 @@ export function resetAllSafetyState(): void {
export interface SafetyStateSnapshot {
toolResultStore: Map<string, { toolName: string; fullContent: string; timestamp: number }>;
toolCallHistory: string[];
userGoal: string;
toolCallTimestamps: Map<string, number[]>;
toolFailureCount: Map<string, number>;
circuitBreakerTripped: Map<string, number>;
errorPatterns: Map<string, ErrorPatternEntry>;
resultFingerprints: ToolResultFingerprint[];
loopTimings: LoopTiming[];
}
@@ -1041,12 +698,7 @@ export function snapshotSafetyState(): SafetyStateSnapshot {
return {
toolResultStore: new Map(_toolResultStore),
toolCallHistory: [..._toolCallHistory],
userGoal: _userGoal,
toolCallTimestamps: new Map(_toolCallTimestamps),
toolFailureCount: new Map(_toolFailureCount),
circuitBreakerTripped: new Map(_circuitBreakerTripped),
errorPatterns: new Map(_errorPatterns),
resultFingerprints: [..._resultFingerprints],
loopTimings: [..._loopTimings],
};
}
@@ -1057,17 +709,8 @@ export function restoreSafetyState(snapshot: SafetyStateSnapshot): void {
snapshot.toolResultStore.forEach((v, k) => _toolResultStore.set(k, v));
_toolCallHistory.length = 0;
_toolCallHistory.push(...snapshot.toolCallHistory);
_userGoal = snapshot.userGoal;
_toolCallTimestamps.clear();
snapshot.toolCallTimestamps.forEach((v, k) => _toolCallTimestamps.set(k, [...v]));
_toolFailureCount.clear();
snapshot.toolFailureCount.forEach((v, k) => _toolFailureCount.set(k, v));
_circuitBreakerTripped.clear();
snapshot.circuitBreakerTripped.forEach((v, k) => _circuitBreakerTripped.set(k, v));
_errorPatterns.clear();
snapshot.errorPatterns.forEach((v, k) => _errorPatterns.set(k, v));
_resultFingerprints.length = 0;
_resultFingerprints.push(...snapshot.resultFingerprints);
_loopTimings.length = 0;
_loopTimings.push(...snapshot.loopTimings);
}
@@ -1499,86 +1142,9 @@ export function autoTuneMemorySearch(
}
// ═══════════════════════════════════════════════════════════════
// R119: 工具执行队列优先级 — 按重要性排序工具执行
// R119 已删除:工具优先级排序强制重排可能打乱 AI 设计的执行顺序
// ═══════════════════════════════════════════════════════════════
export type ToolPriority = 'critical' | 'high' | 'normal' | 'low';
const TOOL_PRIORITY_MAP: Record<string, ToolPriority> = {
// 关键:用户直接请求的操作
write_file: 'critical',
edit_file: 'critical',
delete_file: 'high',
// 高:信息获取类
read_file: 'high',
list_directory: 'high',
search_files: 'high',
// 普通:辅助工具
run_command: 'normal',
web_search: 'normal',
web_fetch: 'normal',
git: 'normal',
// 低:非关键
memory: 'low',
datetime: 'low',
calculator: 'low',
random: 'low',
uuid: 'low',
};
/** R119: 获取工具优先级 */
export function getToolPriority(toolName: string): ToolPriority {
return TOOL_PRIORITY_MAP[toolName] || 'normal';
}
/** R119: 按优先级排序工具调用 */
export function sortToolsByPriority<T extends { name: string }>(
tools: T[]
): T[] {
const priorityOrder: Record<ToolPriority, number> = {
critical: 0,
high: 1,
normal: 2,
low: 3,
};
return [...tools].sort((a, b) => {
const pa = priorityOrder[getToolPriority(a.name)];
const pb = priorityOrder[getToolPriority(b.name)];
return pa - pb;
});
}
/** R119: 按优先级分批(同优先级可并行) */
export function batchToolsByPriority<T extends { name: string }>(
tools: T[]
): T[][] {
const batches: T[][] = [];
const sorted = sortToolsByPriority(tools);
let currentBatch: T[] = [];
let currentPriority: ToolPriority | null = null;
for (const tool of sorted) {
const priority = getToolPriority(tool.name);
if (currentPriority === null) {
currentPriority = priority;
}
if (priority !== currentPriority) {
if (currentBatch.length > 0) batches.push(currentBatch);
currentBatch = [];
currentPriority = priority;
}
currentBatch.push(tool);
}
if (currentBatch.length > 0) batches.push(currentBatch);
return batches;
}
// ═══════════════════════════════════════════════════════════════
// R124: 压缩上下文中工具引用解析 — 恢复被压缩的工具结果引用
// ═══════════════════════════════════════════════════════════════
-298
View File
@@ -1,298 +0,0 @@
/**
* Completion Gate
* Harness Engineering: Agent
*
*
* - 100%
* - AI
* -
*/
import { logWarn, logInfo } from './log-service.js';
import { estimateTokens } from './context-manager.js';
import type { LoopContext, CompletionCheck } from '../types.js';
// ═══════════════════════════════════════════════════════════════
// 内置检查项
// ═══════════════════════════════════════════════════════════════
/** 工具结果审查:是否存在未处理的工具错误 */
const toolResultReviewCheck: CompletionCheck = {
name: 'toolResultReview',
description: '检查是否存在未处理的工具执行错误',
check: async (ctx: LoopContext) => {
const errorRecords = ctx.allToolRecords.filter(r => r.status === 'error');
if (errorRecords.length === 0) return { passed: true, reason: '' };
// 检查这些错误是否在最近的上下文中被讨论过
const recentContent = ctx.messages.slice(-5).map(m => m.content || '').join(' ');
const unresolvedErrors = errorRecords.filter(r => {
const errorName = r.name;
return !recentContent.includes(errorName + ' 失败')
&& !recentContent.includes(errorName + ' error')
&& !recentContent.includes(errorName + ' 错误');
});
if (unresolvedErrors.length > 0) {
return {
passed: false,
reason: `存在 ${unresolvedErrors.length} 个未处理的工具错误: ${unresolvedErrors.map(r => r.name).join(', ')}。请说明这些错误的影响或提供替代方案。`
};
}
return { passed: true, reason: '' };
},
};
/** 思考阶段检测:回复不应仍在"思考中"状态 */
const notThinkingCheck: CompletionCheck = {
name: 'notThinking',
description: '检查回复是否已从思考阶段过渡到结论阶段',
check: async (ctx: LoopContext) => {
// 仅检查短回复(<200字)——长回复几乎肯定是完整回答
if (ctx.content.length > 200) return { passed: true, reason: '' };
const contentLower = ctx.content.slice(0, 100).toLowerCase();
const thinkingMarkers = ['让我看看', '观察一下', '先检查一下', '我需要先', '正在分析', '让我再'];
const continuationSignals = ctx.content.endsWith('...') || ctx.content.endsWith('等等') || ctx.content.endsWith('…');
const conclusionMarkers = [
/Final\s*Answer/i, /最终答案/, /最终回答/, /总结/, /任务完成/, /以上就是/,
/以上[是为]/, /我的能力/, /可以帮你/, /能够/, /我是/, /以下[是为]/,
];
const hasConclusion = conclusionMarkers.some(p => p.test(ctx.content));
if (hasConclusion) return { passed: true, reason: '' };
const thinkingCount = thinkingMarkers.filter(m => contentLower.includes(m)).length;
const isClearlyThinking = thinkingCount >= 2 || (thinkingCount >= 1 && continuationSignals);
if (isClearlyThinking) {
return {
passed: false,
reason: '回复看起来仍在思考阶段(短回复 + 思考动词 + 无结论标志),请基于已有结果给出明确的结论。'
};
}
return { passed: true, reason: '' };
},
};
/** 上下文效率检查:是否存在过度调用工具 */
const contextEfficiencyCheck: CompletionCheck = {
name: 'contextEfficiency',
description: '检查工具调用效率是否合理(避免过度调用)',
check: async (ctx: LoopContext) => {
const toolMessages = ctx.messages.filter(m => m.role === 'tool');
const nonToolMessages = ctx.messages.filter(m => m.role !== 'tool' && m.role !== 'system');
if (toolMessages.length > nonToolMessages.length * 3 && ctx.loopCount > 5) {
return {
passed: false,
reason: `工具调用过多(${toolMessages.length} 次 vs ${nonToolMessages.length} 次非工具消息),可能存在重复或低效的工具调用。请检查是否可以直接基于已有结果给出回答。`
};
}
return { passed: true, reason: '' };
},
};
/** R83: Plan Mode 完成检查 — 验证所有计划步骤是否已标记完成 */
const planModeCompletionCheck: CompletionCheck = {
name: 'planModeCompletion',
description: 'Plan Mode 下检查所有计划步骤是否已完成',
check: async (ctx: LoopContext) => {
if (ctx.mode !== 'plan') return { passed: true, reason: '' };
// 从 state 获取 Plan Tracker
try {
const { getPlanTracker } = await import('./tool-registry.js');
const tracker = getPlanTracker();
if (!tracker.active || tracker.steps.length === 0) return { passed: true, reason: '' };
if (tracker.done < tracker.total) {
const remaining = tracker.total - tracker.done;
const undoneSteps = tracker.steps.filter(s => !s.done).map(s => s.label).slice(0, 3);
return {
passed: false,
reason: `Plan Mode: 还有 ${remaining} 步未完成(${undoneSteps.join('、')}${remaining > 3 ? '...' : ''})。请继续执行剩余步骤,或说明为什么这些步骤无法完成。`
};
}
return { passed: true, reason: '' };
} catch {
return { passed: true, reason: '' };
}
},
};
// ═══════════════════════════════════════════════════════════════
// 检查项注册与管理
// ═══════════════════════════════════════════════════════════════
/** 默认启用的检查项 */
const DEFAULT_ENABLED_CHECKS = new Set([
'toolResultReview',
'notThinking',
'planModeCompletion',
]);
/** 注册表 */
const checkRegistry = new Map<string, CompletionCheck>([
['toolResultReview', toolResultReviewCheck],
['notThinking', notThinkingCheck],
['contextEfficiency', contextEfficiencyCheck],
['planModeCompletion', planModeCompletionCheck],
]);
/** 已启用的检查项 */
const enabledChecks = new Set(DEFAULT_ENABLED_CHECKS);
/** 注册自定义检查项 */
export function registerCompletionCheck(check: CompletionCheck): void {
checkRegistry.set(check.name, check);
logInfo(`Completion Gate: 注册检查项 "${check.name}"`);
}
/** 移除检查项 */
export function unregisterCompletionCheck(name: string): void {
checkRegistry.delete(name);
enabledChecks.delete(name);
}
/** 启用/禁用检查项 */
export function setCheckEnabled(name: string, enabled: boolean): void {
if (enabled) {
if (checkRegistry.has(name)) enabledChecks.add(name);
} else {
enabledChecks.delete(name);
}
}
/** 获取所有已注册检查项 */
export function getRegisteredChecks(): CompletionCheck[] {
return Array.from(checkRegistry.values());
}
/** 获取已启用的检查项 */
export function getEnabledChecks(): CompletionCheck[] {
return getRegisteredChecks().filter(c => enabledChecks.has(c.name));
}
// ═══════════════════════════════════════════════════════════════
// 执行门控
// ═══════════════════════════════════════════════════════════════
export interface GateResult {
passed: boolean;
reason: string;
/** R90: 各项检查的详细结果 */
details: Array<{ name: string; passed: boolean; reason: string; durationMs: number }>;
/** R90/R127: 完成门控评分(0-100 */
score: number;
/** R90/R127: 评分明细 */
scoreBreakdown: Array<{ check: string; passed: boolean; points: number }>;
}
/** R127: 各检查项的权重分配 */
const CHECK_WEIGHTS: Record<string, number> = {
toolResultReview: 25,
notThinking: 20,
contextEfficiency: 15,
planModeCompletion: 10,
};
/**
*
* R90: 返回评分结果
*/
export async function runCompletionGate(ctx: LoopContext): Promise<GateResult> {
const checks = getEnabledChecks();
if (checks.length === 0) {
return { passed: true, reason: '', details: [], score: 100, scoreBreakdown: [] };
}
const details: GateResult['details'] = [];
const scoreBreakdown: GateResult['scoreBreakdown'] = [];
let totalScore = 0;
let maxScore = 0;
for (const check of checks) {
const start = Date.now();
try {
const result = await check.check(ctx);
const durationMs = Date.now() - start;
const weight = CHECK_WEIGHTS[check.name] ?? 10;
maxScore += weight;
details.push({
name: check.name,
passed: result.passed,
reason: result.reason,
durationMs,
});
scoreBreakdown.push({
check: check.name,
passed: result.passed,
points: result.passed ? weight : 0,
});
if (result.passed) {
totalScore += weight;
} else {
logWarn(`Completion Gate: "${check.name}" — ${result.reason}`);
return {
passed: false,
reason: result.reason,
details,
score: Math.round((totalScore / Math.max(1, maxScore)) * 100),
scoreBreakdown,
};
}
} catch (err) {
// 检查项本身抛异常:容错不阻塞流程,但语义上标记为未通过且不得分
const weight = CHECK_WEIGHTS[check.name] ?? 10;
maxScore += weight;
const errMsg = `检查异常: ${(err as Error).message}`;
logWarn(`Completion Gate: "${check.name}" 检查项抛出异常 — ${errMsg}`);
details.push({
name: check.name,
passed: false,
reason: errMsg,
durationMs: Date.now() - start,
});
scoreBreakdown.push({
check: check.name,
passed: false,
points: 0,
});
// 注意:此处不 return,继续执行后续检查项(容错设计)
}
}
return {
passed: true,
reason: '',
details,
score: Math.round((totalScore / Math.max(1, maxScore)) * 100),
scoreBreakdown,
};
}
/**
* R127: 生成门控报告/使
*/
export function formatGateReport(result: GateResult): string {
const status = result.passed ? '✅ 通过' : '❌ 未通过';
let report = `Completion Gate ${status} (评分: ${result.score}/100)`;
if (!result.passed) {
report += `${result.reason}`;
}
report += `\n${'─'.repeat(40)}`;
for (const d of result.details) {
const icon = d.passed ? '✅' : '❌';
report += `\n${icon} ${d.name}: ${d.reason || '通过'} (${d.durationMs}ms)`;
}
// R127: 评分明细
if (result.scoreBreakdown.length > 0) {
report += `\n${'─'.repeat(40)}\n评分明细:`;
for (const s of result.scoreBreakdown) {
report += `\n ${s.passed ? '✅' : '❌'} ${s.check}: ${s.points}`;
}
}
return report;
}
+4 -10
View File
@@ -295,19 +295,12 @@ export function scoreMessageImportance(msg: OllamaMessage): number {
/完成|done|✓|success|成功|结果|result/i,
/项目|project|工作空间|workspace|git|repo|仓库/i,
];
// R10: 修复低价值模式误报 — 移除单字“好”(匹配几乎所有中文文本
// 仅匹配明确的短语回复,且仅对短消息(<100字)生效
const lowValuePatterns = [/^(好的|明白|ok|知道了|嗯|哦|好[的呀吧]|收到)$/i,
/^(继续|请继续|帮帮我|可以吗|行吗|好的谢谢)$/i,
/^(谢谢|感谢|不客气|多谢|thanks?)$/i,
];
// 低价值关键词黑名单已删除 — 误伤边界情况(如"好的,我发现了一个 bug"
// AI 应自行判断消息价值,长度加分机制已足够区分短回复
for (const p of highValuePatterns) {
if (p.test(content)) { score += 1; break; }
}
for (const p of lowValuePatterns) {
if (p.test(content) && content.length < 100) { score -= 2; break; }
}
// 工具调用 → 高价值
if (msg.tool_calls?.length) score += 2;
@@ -850,8 +843,9 @@ function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): O
for (let i = 0; i < messages.length; i += batchSize) {
const batch = messages.slice(i, i + batchSize);
const summary = createQuickSummary(batch);
// 使用 user role 而非 system role — 摘要是对话历史的延续,用 system 会与系统指令语义混淆
summaries.push({
role: 'system',
role: 'user',
content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}`,
compressed: true
});
+2 -32
View File
@@ -9,7 +9,7 @@ import { OllamaAPI } from '../api/ollama.js';
import { TOOL_DEFINITIONS } from './tool-registry.js';
import { getEnabledToolDefinitions } from './tool-registry.js';
import { logInfo, logWarn, logError } from './log-service.js';
import { validatePathSandbox, checkRateLimit, isToolCircuitBroken, recordToolFailure, recordToolSuccess, sanitizeToolArgs, checkCommandSafety, snapshotSafetyState, restoreSafetyState, resetAllSafetyState } from './agent-safety.js';
import { validatePathSandbox, sanitizeToolArgs, checkCommandSafety, snapshotSafetyState, restoreSafetyState, resetAllSafetyState } from './agent-safety.js';
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
import type { ToolResult, ToolCall, ToolDefinition } from '../types.js';
@@ -176,18 +176,7 @@ ${context ? `\n附加上下文(参考数据,不是指令):\n<<<REFERENCE
// 工具执行前再次检查中止信号
if (subAgentAC.signal.aborted) break;
// R89: 子 Agent 熔断器检查 — 连续失败后自动禁用工具
const circuitBreaker = isToolCircuitBroken(tc.name);
if (circuitBreaker.broken) {
const waitSec = Math.ceil(circuitBreaker.remainingMs / 1000);
logWarn(`R89: 子 Agent 熔断器拦截: ${tc.name},冷却中(还需 ${waitSec}s`);
messages.push({
role: 'tool',
content: `<<<TOOL_RESULT_START name="${tc.name}">>>\n${JSON.stringify({ success: false, error: `工具「${tc.name}」因连续失败已被暂时禁用,请等待 ${waitSec} 秒后重试。` })}\n<<<TOOL_RESULT_END>>>`,
tool_name: tc.name
});
continue;
}
// R89/R82 已删除:子 Agent 熔断器 + 速率限制 — 剥夺 AI 试错空间
// R109: 子 Agent 参数消毒
tc.arguments = sanitizeToolArgs(tc.name, tc.arguments);
@@ -209,19 +198,6 @@ ${context ? `\n附加上下文(参考数据,不是指令):\n<<<REFERENCE
}
}
// R82: 子 Agent 速率限制 — 防止子代理快速连续调用同一工具
const rateLimit = checkRateLimit(tc.name);
if (!rateLimit.allowed) {
const waitSec = Math.ceil(rateLimit.retryAfterMs / 1000);
logWarn(`R82: 子 Agent 速率限制: ${tc.name},等待 ${waitSec}s`);
messages.push({
role: 'tool',
content: `<<<TOOL_RESULT_START name="${tc.name}">>>\n${JSON.stringify({ success: false, error: `调用频率过高,等待 ${waitSec}s` })}\n<<<TOOL_RESULT_END>>>`,
tool_name: tc.name
});
continue;
}
// R81: 子 Agent 路径沙箱 — 确保文件操作不超出工作空间
const SUB_FILE_TOOLS = new Set(['read_file', 'list_directory', 'search_files', 'web_fetch']);
if (SUB_FILE_TOOLS.has(tc.name)) {
@@ -246,12 +222,6 @@ ${context ? `\n附加上下文(参考数据,不是指令):\n<<<REFERENCE
try {
const { executeTool } = await import('./tool-registry.js');
const result = await executeTool(tc.name, tc.arguments);
// R89: 记录工具成功/失败到熔断器
if (result.success) {
recordToolSuccess(tc.name);
} else {
recordToolFailure(tc.name);
}
const resultStr = formatResult(tc.name, result);
messages.push({
role: 'tool',
+7 -3
View File
@@ -893,7 +893,10 @@ function hasPathTraversal(path: string): boolean {
return false;
}
/** R28: 命令注入检测 — 检查命令中是否包含危险的 shell 注入模式 */
/** R28: 危险命令关键词 — 命令替换中包含这些词才拦截 */
const DANGEROUS_CMD_KEYWORDS = 'rm|del|format|mkfs|dd|fdisk|shred|umount|chmod\\s+777|chown|shutdown|reboot|halt';
/** R28: 命令注入检测 — 仅拦截命令替换中包含危险命令的情况,避免误伤 $(date) 等合法用法 */
function hasCommandInjection(command: string): boolean {
if (!command) return false;
const dangerousPatterns = [
@@ -901,8 +904,9 @@ function hasCommandInjection(command: string): boolean {
/\|\s*(rm|del|format|mkfs)\s/i, // 管道到删除操作
/&&\s*(rm|del|format|mkfs)\s/i, // AND链中的删除操作
/\$\{.*IFS.*\}/i, // IFS 变量注入
/\$\([^)]*\)/, // 命令替换 $(...)
/`[^`]*`/, // 反引号命令替换
// 仅拦截命令替换中包含危险命令的情况,放行 $(date) 等合法用法
new RegExp(`\\$\\([^)]*\\b(?:${DANGEROUS_CMD_KEYWORDS})\\b[^)]*\\)`, 'i'),
new RegExp('`[^`]*\\b(?:' + DANGEROUS_CMD_KEYWORDS + ')\\b[^`]*`', 'i'),
/\x00/, // null 字节注入
];
return dangerousPatterns.some(p => p.test(command));
-12
View File
@@ -413,10 +413,6 @@ export interface LoopContext {
};
/** R8 紧急压缩重试计数(防止溢出→压缩→重试无限循环) */
emergencyCompressCount: number;
/** Completion Gate 连续失败次数(达到上限后不再重试) */
completionGateFailCount: number;
/** 工具结果核验警告列表(供 Completion Gate 使用) */
verifyWarnings: string[];
/** 上下文压缩标记 — 防止同一轮重复触发压缩 */
compressedThisCycle: boolean;
}
@@ -463,19 +459,11 @@ export interface HookData {
[key: string]: unknown;
}
/** 完成门控检查项 */
export interface CompletionCheck {
name: string;
description: string;
check: (ctx: LoopContext) => Promise<{ passed: boolean; reason: string }>;
}
/** Agent 度量指标 */
export interface AgentMetrics {
totalSessions: number;
avgIterationsPerTask: number;
toolSuccessRate: number;
avgCompletionScore: number;
frequentErrors: Array<{ pattern: string; count: number }>;
tokenEfficiency: number;
collectedAt: number;