后端修复: 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正确填充
285 lines
9.6 KiB
TypeScript
285 lines
9.6 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);
|
||
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);
|
||
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();
|
||
}
|
||
}
|