v0.1.2: 全项目代码审查修复 + 子代理编排器 + 上下文压缩 + 并行工具执行
后端修复: Ollama adapter 移除死代码/修复超时硬编码/iteration硬编码/pullModel无超时/流异常断开补发DONE; SSE解析器支持非字符串arguments; Sandbox fail-closed安全加固; IPC移除未使用变量 新增功能: TaskOrchestrator子任务编排器; DelegateTaskTool委派工具; Agent Loop并行工具执行; 上下文自动压缩; 记忆检索注入System Prompt 前端修复: useAgentStream text_delta thought累积bug; 工具调用状态正确流转; 修复重复stateChange事件; TraceStep.thought正确填充
This commit is contained in:
@@ -2,17 +2,26 @@
|
||||
* Task Orchestrator — 任务编排器
|
||||
*
|
||||
* 支持父子委派模式:主 Agent 委派子任务给 SubAgent。
|
||||
* 每个子任务运行一个独立的 AgentLoopEngine 实例。
|
||||
* 每个 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 type { AgentLoopEngine } from '../agent-loop/engine';
|
||||
import type { MetonaMessage, MetonaSystemPrompt } from '../types';
|
||||
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 { MetonaToolDef } from '../types';
|
||||
import type { PreToolHook } from '../hooks/pre-tool';
|
||||
import type { PostToolHook } from '../hooks/post-tool';
|
||||
import log from 'electron-log';
|
||||
|
||||
export interface SubAgentResult {
|
||||
@@ -20,25 +29,34 @@ export interface SubAgentResult {
|
||||
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;
|
||||
onComplete: (callback: (result: SubAgentResult) => void) => void;
|
||||
onError: (callback: (error: Error) => void) => void;
|
||||
getStatus: () => { taskId: string; status: string; description: string };
|
||||
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 agentLoopEngine: AgentLoopEngine,
|
||||
private mainEngine: AgentLoopEngine,
|
||||
private toolRegistry?: ToolRegistry,
|
||||
private preToolHooks: PreToolHook[] = [],
|
||||
private postToolHooks: PostToolHook[] = [],
|
||||
private defaultConfig?: Partial<AgentLoopConfig>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -46,8 +64,8 @@ export class TaskOrchestrator extends EventEmitter {
|
||||
/**
|
||||
* 委派子任务
|
||||
*
|
||||
* 创建一个独立的 SubAgent 执行上下文,调用 AgentLoopEngine 完成任务。
|
||||
* 支持限制可用工具列表(白名单)。
|
||||
* 创建一个独立的 AgentLoopEngine 实例执行子任务。
|
||||
* SubAgent 不共享主 Agent 的引擎状态,安全隔离。
|
||||
*/
|
||||
async delegate(params: {
|
||||
taskId?: string;
|
||||
@@ -59,25 +77,60 @@ export class TaskOrchestrator extends EventEmitter {
|
||||
const taskId = params.taskId ?? `sub_${nanoid(8)}`;
|
||||
const startMs = Date.now();
|
||||
|
||||
this.emit('taskDelegated', { taskId, description: params.description, parentSessionId: params.parentSessionId });
|
||||
// ===== 递归深度检查 =====
|
||||
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);
|
||||
},
|
||||
onComplete: (callback) => {
|
||||
if (handle.result) callback(handle.result);
|
||||
},
|
||||
onError: (callback) => { /* errors captured by try/catch below */ },
|
||||
getStatus: () => ({ taskId, status: handle.status, description: handle.description }),
|
||||
getStatus: () => ({ taskId, status: handle.status, description: handle.description, depth: handle.depth }),
|
||||
};
|
||||
|
||||
this.activeSubAgents.set(taskId, handle);
|
||||
this.emit('taskStarted', { taskId });
|
||||
this.emit('taskStarted', { taskId, depth });
|
||||
|
||||
try {
|
||||
// 构建用户消息
|
||||
@@ -87,49 +140,16 @@ export class TaskOrchestrator extends EventEmitter {
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 构建 System Prompt(子 Agent 简化版)
|
||||
const systemPrompt: MetonaSystemPrompt = {
|
||||
roleDefinition: '你是一个子任务执行 Agent,负责完成被委派的单一任务。',
|
||||
outputConstraints: '用中文回答,简洁准确地完成任务。',
|
||||
safetyGuidelines: '不访问工作空间外的文件,不执行危险命令。',
|
||||
};
|
||||
|
||||
// 保存原始工具列表(子任务完成后恢复)
|
||||
const savedTools = this.agentLoopEngine.getTools?.() ?? [];
|
||||
|
||||
// 如果指定了工具白名单,设置受限工具集
|
||||
if (params.tools && params.tools.length > 0 && this.toolRegistry) {
|
||||
const allowedTools: MetonaToolDef[] = [];
|
||||
for (const toolName of params.tools) {
|
||||
const tool = this.toolRegistry.get(toolName);
|
||||
if (tool) {
|
||||
allowedTools.push({
|
||||
name: tool.definition.name,
|
||||
description: tool.definition.description,
|
||||
parameters: tool.definition.parameters,
|
||||
category: tool.definition.category,
|
||||
riskLevel: tool.definition.riskLevel,
|
||||
requiresPermission: tool.definition.requiresPermission,
|
||||
timeoutMs: tool.definition.timeoutMs,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.agentLoopEngine.setTools(allowedTools);
|
||||
}
|
||||
// 构建 System Prompt(子 Agent 专用)
|
||||
const systemPrompt = this.buildSubAgentPrompt(params.description, depth);
|
||||
|
||||
// 运行 Agent Loop(同步等待完成)
|
||||
let output;
|
||||
try {
|
||||
output = await this.agentLoopEngine.runStream(
|
||||
userMessage,
|
||||
taskId,
|
||||
[], // 子 Agent 无历史
|
||||
systemPrompt,
|
||||
);
|
||||
} finally {
|
||||
// 恢复主 Agent 的原始工具列表
|
||||
this.agentLoopEngine.setTools(savedTools);
|
||||
}
|
||||
const output = await subEngine.runStream(
|
||||
userMessage,
|
||||
taskId,
|
||||
[], // 子 Agent 无历史
|
||||
systemPrompt,
|
||||
);
|
||||
|
||||
const durationMs = Date.now() - startMs;
|
||||
const success = output.terminationReason === 'completed';
|
||||
@@ -138,14 +158,16 @@ export class TaskOrchestrator extends EventEmitter {
|
||||
result: output.finalAnswer,
|
||||
success,
|
||||
durationMs,
|
||||
iterations: output.iterations.length,
|
||||
};
|
||||
|
||||
handle.status = success ? 'completed' : 'error';
|
||||
handle.result = result;
|
||||
this.activeSubAgents.delete(taskId);
|
||||
this.sessionDepth.set(params.parentSessionId, currentDepth); // 恢复深度
|
||||
this.emit('taskCompleted', result);
|
||||
|
||||
log.info(`[Orchestrator] SubAgent "${taskId}" ${success ? 'completed' : 'failed'} in ${durationMs}ms`);
|
||||
log.info(`[Orchestrator] SubAgent "${taskId}" (depth=${depth}) ${success ? 'completed' : 'failed'} in ${durationMs}ms, ${output.iterations.length} iterations`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -156,40 +178,107 @@ export class TaskOrchestrator extends EventEmitter {
|
||||
result: errMsg,
|
||||
success: false,
|
||||
durationMs,
|
||||
iterations: 0,
|
||||
};
|
||||
|
||||
handle.status = 'error';
|
||||
handle.result = result;
|
||||
this.activeSubAgents.delete(taskId);
|
||||
this.sessionDepth.set(params.parentSessionId, currentDepth); // 恢复深度
|
||||
this.emit('taskError', { taskId, error: errMsg });
|
||||
|
||||
log.error(`[Orchestrator] SubAgent "${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 };
|
||||
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 }> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user