大版本迭代,新增安全增强、Agent Loop 增强、UI/UX 增强,经六轮全面审计修复所有问题。
新增功能:
- OutputValidator 事实一致性检查 + 幻觉检测
- PromptInjectionDefender 语义级检测(指令性动词密度、角色边界、分隔符嵌套)
- DeadLoopError 死循环检测(连续3轮相同工具调用自动终止)
- PolicyEngine maxFrequency 滑动窗口频率限制
- MemoryManager TF-IDF 语义检索 + IDF 缓存原子替换
- 斜杠命令(/tool /memory /clear /export)+ 快捷键(Ctrl+B/J/Shift+F/N/[/])
- 专注模式(Ctrl+Shift+F)带面板状态快照保存/恢复
六轮审计修复(共修复 3 CRITICAL + 8 HIGH + 13 MEDIUM + 11 LOW):
CRITICAL:
- main.ts 传入 createAdapter 而非 reloadAdapter,导致 Provider 切换完全失效
- Ctrl+N/Ctrl+[/Ctrl+] 不同步 agent-store,导致消息发到错误会话
- engine.ts emit('error') 无监听器导致 DONE 事件丢失、前端卡死
HIGH:
- 死循环检测在工具执行之后(移入 PARSING 后 EXECUTING 前)
- deadLoop 事件前端未处理
- ConfirmationHook.clearPending 从未调用导致定时器泄漏
- engine.ts retry abort listener 未移除导致监听器堆积
- MCPManager JSON.parse 无 try-catch 导致初始化崩溃
- sendMessage 自动创建会话不同步 session-store
- setCurrentSession 竞态导致旧请求覆盖新会话数据
MEDIUM:
- toggleFocusMode 覆盖用户原有面板状态
- 正则检测可被常见词绕过
- 频率限制内存泄漏 + customPolicies 覆盖
- LIKE 回退转义未包含反斜杠
- OutputValidator 新功能未传入 toolResults/context
- MemoryConsolidator LLM 调用无超时保护
- AuditService 每次 log 都查询数据库
- browser-window-manager 超时后未停止页面加载
- output-validator URL 比较大小写敏感
- FileReader 无 onerror 导致 Promise 永久挂起
- /clear /export 不关闭斜杠菜单
- 专注模式下 toggleSidebar/toggleDetail 未恢复另一面板快照
LOW:
- abortPromise 事件监听器堆积
- RateLimitHook Map 内存泄漏
- memory.ts await 同步方法
- PolicyEngine 死代码清理
- Orchestrator sessionDepth 会话结束不清理
- workspace.service.ts 元数据插入边界问题
295 lines
10 KiB
TypeScript
295 lines
10 KiB
TypeScript
/**
|
||
* Task Orchestrator — 任务编排器
|
||
*
|
||
* 支持父子委派模式:主 Agent 委派子任务给 SubAgent。
|
||
* 每个 SubAgent 运行在**独立的 AgentLoopEngine 实例**中,避免状态污染。
|
||
*
|
||
* 安全保障:
|
||
* 1. 独立引擎实例 — SubAgent 不共享主 Agent 的引擎状态
|
||
* 2. 递归深度限制 — 默认最大 3 层,防止无限递归
|
||
* 3. 工具白名单隔离 — SubAgent 默认不继承 delegate_task(防止递归)
|
||
* 4. 真正的 abort — 通过引擎引用调用 engine.abort()
|
||
* 5. 事件隔离 — SubAgent 的流式事件不直接转发到前端,仅通过 orchestrator 事件通知
|
||
*
|
||
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第五章
|
||
*/
|
||
|
||
import { EventEmitter } from 'events';
|
||
import { nanoid } from 'nanoid';
|
||
import { AgentLoopEngine } from '../agent-loop/engine';
|
||
import type { AgentLoopConfig } from '../agent-loop/types';
|
||
import type { MetonaMessage, MetonaSystemPrompt, MetonaToolDef } from '../types';
|
||
import type { ToolRegistry } from '../tools/registry';
|
||
import type { PreToolHook } from '../hooks/pre-tool';
|
||
import type { PostToolHook } from '../hooks/post-tool';
|
||
import log from 'electron-log';
|
||
|
||
export interface SubAgentResult {
|
||
taskId: string;
|
||
result: string;
|
||
success: boolean;
|
||
durationMs: number;
|
||
iterations: number;
|
||
}
|
||
|
||
interface SubAgentHandle {
|
||
taskId: string;
|
||
description: string;
|
||
status: 'pending' | 'running' | 'completed' | 'error';
|
||
depth: number;
|
||
engine?: AgentLoopEngine;
|
||
result?: SubAgentResult;
|
||
abort: () => void;
|
||
getStatus: () => { taskId: string; status: string; description: string; depth: number };
|
||
}
|
||
|
||
/** 默认递归深度限制 */
|
||
const MAX_DELEGATION_DEPTH = 3;
|
||
|
||
export class TaskOrchestrator extends EventEmitter {
|
||
private activeSubAgents = new Map<string, SubAgentHandle>();
|
||
/** 追踪每个 session 的当前委派深度 */
|
||
private sessionDepth = new Map<string, number>();
|
||
|
||
constructor(
|
||
private mainEngine: AgentLoopEngine,
|
||
private toolRegistry?: ToolRegistry,
|
||
private preToolHooks: PreToolHook[] = [],
|
||
private postToolHooks: PostToolHook[] = [],
|
||
private defaultConfig?: Partial<AgentLoopConfig>,
|
||
) {
|
||
super();
|
||
}
|
||
|
||
/**
|
||
* 委派子任务
|
||
*
|
||
* 创建一个独立的 AgentLoopEngine 实例执行子任务。
|
||
* SubAgent 不共享主 Agent 的引擎状态,安全隔离。
|
||
*/
|
||
async delegate(params: {
|
||
taskId?: string;
|
||
description: string;
|
||
parentSessionId: string;
|
||
maxIterations?: number;
|
||
tools?: string[];
|
||
}): Promise<SubAgentResult> {
|
||
const taskId = params.taskId ?? `sub_${nanoid(8)}`;
|
||
const startMs = Date.now();
|
||
|
||
// ===== 递归深度检查 =====
|
||
const currentDepth = this.sessionDepth.get(params.parentSessionId) ?? 0;
|
||
if (currentDepth >= MAX_DELEGATION_DEPTH) {
|
||
log.warn(`[Orchestrator] Delegation depth limit reached (${currentDepth}) for session ${params.parentSessionId}`);
|
||
return {
|
||
taskId,
|
||
result: `SubAgent delegation depth limit reached (${MAX_DELEGATION_DEPTH}). Cannot delegate further.`,
|
||
success: false,
|
||
durationMs: 0,
|
||
iterations: 0,
|
||
};
|
||
}
|
||
const depth = currentDepth + 1;
|
||
this.sessionDepth.set(params.parentSessionId, depth);
|
||
|
||
this.emit('taskDelegated', { taskId, description: params.description, parentSessionId: params.parentSessionId, depth });
|
||
|
||
// ===== 创建独立的引擎实例 =====
|
||
const subEngine = new AgentLoopEngine(
|
||
{
|
||
maxIterations: params.maxIterations ?? 10,
|
||
totalTimeoutMs: 300_000, // 子任务总超时 5 分钟
|
||
thinkingEnabled: this.defaultConfig?.thinkingEnabled ?? true,
|
||
thinkingEffort: this.defaultConfig?.thinkingEffort ?? 'medium',
|
||
contextLength: this.defaultConfig?.contextLength,
|
||
contextWindow: this.defaultConfig?.contextWindow ?? 128_000,
|
||
},
|
||
this.mainEngine.getAdapter(),
|
||
this.toolRegistry,
|
||
this.preToolHooks,
|
||
this.postToolHooks,
|
||
);
|
||
subEngine.setWorkspacePath(this.mainEngine.getWorkspacePath());
|
||
|
||
// ===== 工具白名单设置 =====
|
||
const allowedTools = this.resolveTools(params.tools);
|
||
subEngine.setTools(allowedTools);
|
||
|
||
const handle: SubAgentHandle = {
|
||
taskId,
|
||
description: params.description,
|
||
status: 'running',
|
||
depth,
|
||
engine: subEngine,
|
||
abort: () => {
|
||
subEngine.abort();
|
||
handle.status = 'error';
|
||
this.activeSubAgents.delete(taskId);
|
||
},
|
||
getStatus: () => ({ taskId, status: handle.status, description: handle.description, depth: handle.depth }),
|
||
};
|
||
|
||
this.activeSubAgents.set(taskId, handle);
|
||
this.emit('taskStarted', { taskId, depth });
|
||
|
||
try {
|
||
// 构建用户消息
|
||
const userMessage: MetonaMessage = {
|
||
role: 'user',
|
||
content: params.description,
|
||
timestamp: Date.now(),
|
||
};
|
||
|
||
// 构建 System Prompt(子 Agent 专用)
|
||
const systemPrompt = this.buildSubAgentPrompt(params.description, depth);
|
||
|
||
// 运行 Agent Loop(同步等待完成)
|
||
const output = await subEngine.runStream(
|
||
userMessage,
|
||
taskId,
|
||
[], // 子 Agent 无历史
|
||
systemPrompt,
|
||
);
|
||
|
||
const durationMs = Date.now() - startMs;
|
||
const success = output.terminationReason === 'completed';
|
||
const result: SubAgentResult = {
|
||
taskId,
|
||
result: output.finalAnswer,
|
||
success,
|
||
durationMs,
|
||
iterations: output.iterations.length,
|
||
};
|
||
|
||
handle.status = success ? 'completed' : 'error';
|
||
handle.result = result;
|
||
this.activeSubAgents.delete(taskId);
|
||
// v0.3.0 修复: 深度恢复为 0 时删除条目,避免 sessionDepth Map 随会话累积
|
||
if (currentDepth === 0) {
|
||
this.sessionDepth.delete(params.parentSessionId);
|
||
} else {
|
||
this.sessionDepth.set(params.parentSessionId, currentDepth);
|
||
}
|
||
this.emit('taskCompleted', result);
|
||
|
||
log.info(`[Orchestrator] SubAgent "${taskId}" (depth=${depth}) ${success ? 'completed' : 'failed'} in ${durationMs}ms, ${output.iterations.length} iterations`);
|
||
|
||
return result;
|
||
} catch (error) {
|
||
const durationMs = Date.now() - startMs;
|
||
const errMsg = (error as Error).message;
|
||
const result: SubAgentResult = {
|
||
taskId,
|
||
result: errMsg,
|
||
success: false,
|
||
durationMs,
|
||
iterations: 0,
|
||
};
|
||
|
||
handle.status = 'error';
|
||
handle.result = result;
|
||
this.activeSubAgents.delete(taskId);
|
||
// v0.3.0 修复: 深度恢复为 0 时删除条目,避免 sessionDepth Map 随会话累积
|
||
if (currentDepth === 0) {
|
||
this.sessionDepth.delete(params.parentSessionId);
|
||
} else {
|
||
this.sessionDepth.set(params.parentSessionId, currentDepth);
|
||
}
|
||
this.emit('taskError', { taskId, error: errMsg });
|
||
|
||
log.error(`[Orchestrator] SubAgent "${taskId}" (depth=${depth}) error: ${errMsg}`);
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析工具白名单
|
||
*
|
||
* - 如果指定了 tools,使用白名单(自动排除 delegate_task 防止递归)
|
||
* - 如果未指定,使用所有已启用工具(同样排除 delegate_task)
|
||
*/
|
||
private resolveTools(toolNames?: string[]): MetonaToolDef[] {
|
||
if (!this.toolRegistry) return [];
|
||
|
||
// 始终排除 delegate_task 防止递归(除非深度为 1 且显式要求)
|
||
const EXCLUDE_TOOLS = new Set(['delegate_task']);
|
||
|
||
if (toolNames && toolNames.length > 0) {
|
||
// 使用白名单模式
|
||
const resolved: MetonaToolDef[] = [];
|
||
const notFound: string[] = [];
|
||
|
||
for (const name of toolNames) {
|
||
if (EXCLUDE_TOOLS.has(name)) continue; // 静默排除
|
||
const tool = this.toolRegistry.get(name);
|
||
if (tool) {
|
||
resolved.push(tool.definition);
|
||
} else {
|
||
notFound.push(name);
|
||
}
|
||
}
|
||
|
||
if (notFound.length > 0) {
|
||
log.warn(`[Orchestrator] Tools not found: ${notFound.join(', ')}`);
|
||
}
|
||
|
||
return resolved;
|
||
}
|
||
|
||
// 未指定白名单 — 使用所有已启用工具(排除 delegate_task)
|
||
return this.toolRegistry.listTools().filter((t) => !EXCLUDE_TOOLS.has(t.name));
|
||
}
|
||
|
||
/**
|
||
* 构建 SubAgent 的 System Prompt
|
||
*/
|
||
private buildSubAgentPrompt(description: string, depth: number): MetonaSystemPrompt {
|
||
return {
|
||
roleDefinition: `You are a SubAgent (delegation depth: ${depth}) executing a specific sub-task delegated by the parent Agent.\nYour goal is to complete the assigned task efficiently and return a clear, concise result.\nFocus only on the task at hand. Do not delegate further.`,
|
||
outputConstraints: `Complete the task and provide a clear summary of your findings or actions.\nRespond in the same language as the task description.\nKeep your response focused and relevant — the parent Agent will use your result to continue its work.`,
|
||
safetyGuidelines: `Do not access files outside the workspace.\nDo not execute dangerous commands.\nIf the task cannot be completed, explain why clearly.`,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 中断指定子任务
|
||
*/
|
||
abortTask(taskId: string): boolean {
|
||
const handle = this.activeSubAgents.get(taskId);
|
||
if (handle && handle.status === 'running') {
|
||
handle.abort();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 完成子任务(外部触发,保留接口兼容)
|
||
*/
|
||
completeTask(taskId: string, result: string, success: boolean): void {
|
||
const handle = this.activeSubAgents.get(taskId);
|
||
if (handle && handle.status === 'running') {
|
||
handle.status = success ? 'completed' : 'error';
|
||
handle.result = { taskId, result, success, durationMs: 0, iterations: 0 };
|
||
this.activeSubAgents.delete(taskId);
|
||
this.emit('taskCompleted', handle.result);
|
||
}
|
||
}
|
||
|
||
getActiveAgentsStatus(): Array<{ taskId: string; status: string; description: string; depth: number }> {
|
||
return Array.from(this.activeSubAgents.values()).map((a) => a.getStatus());
|
||
}
|
||
|
||
/**
|
||
* 中断所有子任务
|
||
*/
|
||
abortAll(): void {
|
||
for (const agent of this.activeSubAgents.values()) {
|
||
agent.abort();
|
||
}
|
||
this.activeSubAgents.clear();
|
||
this.sessionDepth.clear();
|
||
}
|
||
}
|