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:
thzxx
2026-07-06 22:48:33 +08:00
parent 8cdb93f8bf
commit 6f08759f63
25 changed files with 1044 additions and 675 deletions
+136 -27
View File
@@ -34,6 +34,7 @@ import type {
MetonaToolDef,
} from '../types';
import { MetonaStreamEventType, MetonaFinishReason } from '../types';
import log from 'electron-log';
const DEFAULT_CONFIG: AgentLoopConfig = {
maxIterations: 20,
@@ -224,6 +225,16 @@ export class AgentLoopEngine extends EventEmitter {
}
}
/** 获取当前 Provider Adapter(供 SubAgent 创建独立引擎实例) */
getAdapter(): IMetonaProviderAdapter {
return this.adapter;
}
/** 获取工作空间路径(供 SubAgent 继承) */
getWorkspacePath(): string {
return this.workspacePath;
}
/** 中断循环 */
abort(): void {
this.aborted = true;
@@ -253,12 +264,8 @@ export class AgentLoopEngine extends EventEmitter {
try {
// === THINKING: 流式调用 LLM ===
// transitionTo 已发射 stateChange 事件,无需重复 emit
await this.transitionTo(AgentLoopState.THINKING);
this.emit('stateChange', {
sessionId,
iteration: this.currentIteration,
state: 'THINKING',
});
let fullContent = '';
let reasoningContent = '';
@@ -362,19 +369,14 @@ export class AgentLoopEngine extends EventEmitter {
// === EXECUTING: 执行工具调用 ===
if (step.toolCalls && step.toolCalls.length > 0) {
// transitionTo 已发射 stateChange 事件,无需重复 emit
await this.transitionTo(AgentLoopState.EXECUTING);
this.emit('stateChange', {
sessionId,
iteration: this.currentIteration,
state: 'EXECUTING',
});
step.toolResults = [];
for (const tc of step.toolCalls) {
// 并行执行所有工具调用(独立工具之间无依赖,可安全并发)
const executeAndForward = async (tc: MetonaToolCall): Promise<MetonaToolResult> => {
const result = await this.executeToolSafely(tc);
step.toolResults.push(result);
// 转发工具结果到渲染进程
// 立即转发工具结果到渲染进程(不等其他工具完成)
this.emit('streamEvent', {
type: MetonaStreamEventType.TOOL_RESULT,
requestId: request.meta.requestId,
@@ -384,25 +386,33 @@ export class AgentLoopEngine extends EventEmitter {
timestamp: Date.now(),
toolResult: result,
});
}
return result;
};
// Promise.all 保持结果顺序与 toolCalls 一致
step.toolResults = await Promise.all(
step.toolCalls.map((tc) => executeAndForward(tc)),
);
}
// === OBSERVING ===
await this.transitionTo(AgentLoopState.OBSERVING);
// === 上下文压缩(每 5 轮检查一次 ===
if (this.currentIteration % 5 === 0) {
// === 上下文压缩(基于 token 使用率触发 ===
const estimatedTokens = this.estimateMessagesTokens(request.messages);
const compressionThreshold = this.config.compressionThreshold * this.config.contextWindow;
if (estimatedTokens > compressionThreshold && request.messages.length > 10) {
await this.transitionTo(AgentLoopState.COMPRESSING);
this.emit('stateChange', {
sessionId,
iteration: this.currentIteration,
state: 'COMPRESSING',
});
// 在 context 中标记压缩点
const keepRecent = 3;
if (this.iterations.length > keepRecent) {
this.emit('compressed', { iteration: this.currentIteration, keptRounds: keepRecent });
const compressed = await this.compressMessages(request.messages);
if (compressed) {
// 原地替换数组内容,确保外层 messages 引用同步更新
request.messages.splice(0, request.messages.length, ...compressed);
this.emit('compressed', {
iteration: this.currentIteration,
originalTokens: estimatedTokens,
compressedTokens: this.estimateMessagesTokens(compressed),
});
}
}
@@ -492,6 +502,95 @@ export class AgentLoopEngine extends EventEmitter {
});
}
/**
* 估算消息列表的 token 数(粗略:1 字符 ≈ 0.5 token
*/
private estimateMessagesTokens(messages: MetonaMessage[]): number {
let total = 0;
for (const msg of messages) {
total += Math.ceil(msg.content.length / 2);
if (msg.reasoningContent) total += Math.ceil(msg.reasoningContent.length / 2);
if (msg.toolCalls) {
for (const tc of msg.toolCalls) {
total += Math.ceil(JSON.stringify(tc.args).length / 2);
}
}
}
return total;
}
/**
* 上下文压缩 — 将旧消息摘要为一条 system 消息,保留最近 3 轮完整对话
*
* 策略:
* 1. 保留最后 keepRecent 条消息(约 3 轮对话)
* 2. 将前面的所有消息交给 LLM 生成摘要
* 3. 用 [Context Summary] system 消息 + 近期消息替换原数组
*
* @returns 压缩后的消息数组,压缩失败时返回 null(调用方保持原数组)
*/
private async compressMessages(messages: MetonaMessage[]): Promise<MetonaMessage[] | null> {
const keepRecent = 10; // 保留最近 10 条消息(约 3 轮 user+assistant+tool
if (messages.length <= keepRecent) return null;
const toCompress = messages.slice(0, messages.length - keepRecent);
const toKeep = messages.slice(messages.length - keepRecent);
// 构建摘要请求
const conversationText = toCompress.map((m) => {
const role = m.role.toUpperCase();
const content = m.content.slice(0, 500); // 每条消息最多 500 字符
return `[${role}] ${content}`;
}).join('\n\n');
const summaryRequest: MetonaRequest = {
meta: {
sessionId: this.currentSessionId,
iteration: this.currentIteration,
requestId: `r_${nanoid(12)}`,
timestamp: Date.now(),
agentVersion: '1.0.0',
},
systemPrompt: {
roleDefinition: 'You are a conversation summarizer.',
outputConstraints: 'Summarize the following conversation history concisely. Preserve key facts, decisions, tool results, and context needed for future reasoning. Output in the same language as the conversation. Maximum 300 words.',
safetyGuidelines: 'Do not include sensitive data like passwords or API keys in the summary.',
},
messages: [{
role: 'user',
content: `Please summarize the following conversation history:\n\n${conversationText}`,
timestamp: Date.now(),
}],
params: {
maxTokens: 2048,
temperature: 0.0,
stream: false,
thinkingEnabled: false,
thinkingEffort: 'low',
},
};
try {
const response = await this.adapter.chat(summaryRequest);
const summary = response.content.trim();
if (!summary) return null;
const summaryMessage: MetonaMessage = {
role: 'system',
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];
} catch (error) {
log.warn('[AgentLoop] Context compression failed, keeping original messages:', (error as Error).message);
return null;
}
}
private accumulateTokens(usage: TokenUsage): void {
this.totalTokens.promptTokens += usage.promptTokens;
this.totalTokens.completionTokens += usage.completionTokens;
@@ -514,6 +613,16 @@ export class AgentLoopEngine extends EventEmitter {
});
this.currentState = AgentLoopState.TERMINATED;
// 发射 complete 事件(供托盘通知等外部监听器使用)
this.emit('complete', {
sessionId: this.currentSessionId,
durationMs: Date.now() - this.startTime,
terminationReason: reason,
iterations: this.iterations.length,
totalTokens: this.totalTokens.totalTokens,
});
return {
finalAnswer: answer ?? (error ? error.message : 'No answer produced'),
terminationReason: reason,