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
+1 -1
View File
@@ -30,7 +30,7 @@ export function TraceViewer(): React.JSX.Element {
) : (
traceSteps.map((step, i) => (
<TraceStep
key={`${step.iteration}-${step.state}-${i}`}
key={step.id}
step={step}
isCurrent={i === traceSteps.length - 1 && agentStatus !== 'idle'}
/>
+138 -70
View File
@@ -1,13 +1,17 @@
/**
* useAgentStream — Agent 流式事件监听 Hook
*
* 监听 window.metona.agent.onStreamEvent,将事件分发到 Zustand Store。
* 负责流式文本追加、工具调用状态更新、思考内容处理。
* 监听 window.metona.agent.onStreamEvent / onStateChange,将事件分发到 Zustand Store。
*
* 架构设计:
* - onStateChange:迭代号追踪 + 消息卡片创建 + Trace 步骤创建 + Agent 状态映射
* (状态变化事件总是先于同迭代的流式内容事件到达,因此在此统一管理迭代边界)
* - onStreamEvent:纯内容更新(reasoning、text、tool_call、tool_result、usage、done、error
* (不再处理迭代号比较,消除竞态条件)
*/
import { useEffect, useRef } from 'react';
import { useAgentStore, type ToolCallInfo } from '@renderer/stores/agent-store';
import { useSessionStore } from '@renderer/stores/session-store';
import { useAgentStore, type ToolCallInfo, type AgentStatus } from '@renderer/stores/agent-store';
/**
* Agent 流式事件监听 Hook
@@ -17,8 +21,8 @@ import { useSessionStore } from '@renderer/stores/session-store';
export function useAgentStream(): void {
const cleanupRef = useRef<(() => void) | null>(null);
// ===== 流式内容事件 =====
useEffect(() => {
// 检查 IPC 桥是否可用
if (!window.metona?.agent?.onStreamEvent) return;
const unsubscribe = window.metona.agent.onStreamEvent((event: unknown) => {
@@ -39,39 +43,7 @@ export function useAgentStream(): void {
// 每次都从 store 读取最新状态(避免闭包捕获过期快照)
const getStore = () => useAgentStore.getState();
// 每次收到迭代号时同步更新
if (data.iteration != null && data.iteration !== getStore().currentIteration) {
const prevIteration = getStore().currentIteration;
getStore().setCurrentIteration(data.iteration);
// 迭代号增大 → 新一轮 ReAct 迭代开始,创建新的 assistant 消息卡片
if (data.iteration > prevIteration && prevIteration > 0) {
const messages = getStore().messages;
const lastMsg = messages[messages.length - 1];
// 仅当上一条 assistant 消息已有内容时才创建新消息(避免空消息堆叠)
if (lastMsg?.role === 'assistant' && (lastMsg.content || lastMsg.toolCalls?.length || lastMsg.reasoningContent)) {
getStore().addMessage({
id: `msg_${Date.now()}_assistant`,
role: 'assistant',
content: '',
timestamp: Date.now(),
iteration: data.iteration,
});
}
}
}
switch (data.type) {
// 思考开始
case 'thinking_start':
getStore().setAgentStatus('thinking');
getStore().addTraceStep({
iteration: data.iteration ?? getStore().currentIteration + 1,
state: 'THINKING',
startedAt: Date.now(),
});
break;
// 推理内容增量
case 'reasoning_delta':
if (data.delta) {
@@ -90,16 +62,18 @@ export function useAgentStream(): void {
content: '',
reasoningContent: data.delta,
timestamp: Date.now(),
iteration: data.iteration ?? getStore().currentIteration,
iteration: getStore().currentIteration || undefined,
});
}
}
break;
// 思考结束
case 'thinking_end':
if (data.iteration) {
getStore().updateTraceStep(data.iteration, { completedAt: Date.now() });
// 同步更新当前 Trace 步骤的 thought 字段
const traceSteps = getStore().traceSteps;
const curStep = traceSteps[traceSteps.length - 1];
if (curStep) {
getStore().updateLastTraceStep({
thought: (curStep.thought ?? '') + data.delta,
});
}
}
break;
@@ -108,6 +82,18 @@ export function useAgentStream(): void {
if (data.delta) {
getStore().setStreaming(true);
getStore().updateLastAssistantMessage(data.delta);
// 无 reasoning 模式下,将文本内容也记入 Trace thought(便于追踪)
// 当 assistant 消息无 reasoningContent 时,持续累积 text delta 到 thought
const messages = getStore().messages;
const lastMsg = messages[messages.length - 1];
const steps = getStore().traceSteps;
const step = steps[steps.length - 1];
if (step && lastMsg?.role === 'assistant' && !lastMsg.reasoningContent) {
getStore().updateLastTraceStep({
thought: (step.thought ?? '') + data.delta,
});
}
}
break;
@@ -118,7 +104,7 @@ export function useAgentStream(): void {
id: data.toolCall.id,
name: data.toolCall.name,
args: data.toolCall.args,
status: 'pending',
status: 'executing',
};
// 追加到当前 assistant 消息
@@ -130,14 +116,14 @@ export function useAgentStream(): void {
});
}
getStore().setAgentStatus('executing');
const curIter = getStore().currentIteration;
getStore().addTraceStep({
iteration: data.iteration ?? curIter,
state: 'EXECUTING',
startedAt: Date.now(),
toolCalls: [tc],
});
// 更新当前 Trace 步骤(添加工具调用信息)
const curSteps = getStore().traceSteps;
const lastStep = curSteps[curSteps.length - 1];
if (lastStep) {
getStore().updateLastTraceStep({
toolCalls: [...(lastStep.toolCalls ?? []), tc],
});
}
}
break;
@@ -160,6 +146,24 @@ export function useAgentStream(): void {
);
getStore().updateMessage(lastMsg.id, { toolCalls: updatedToolCalls });
}
// 同步更新 Trace 步骤中的工具调用状态
const steps = getStore().traceSteps;
const lastTrace = steps[steps.length - 1];
if (lastTrace?.toolCalls) {
const updatedTraceToolCalls = lastTrace.toolCalls.map((tc) =>
tc.id === data.toolResult!.toolCallId
? {
...tc,
status: data.toolResult!.success ? 'success' as const : 'error' as const,
result: data.toolResult!.result,
error: data.toolResult!.error,
durationMs: data.toolResult!.durationMs,
}
: tc,
);
getStore().updateLastTraceStep({ toolCalls: updatedTraceToolCalls });
}
}
break;
}
@@ -173,6 +177,15 @@ export function useAgentStream(): void {
outputTokens: cur.outputTokens + (data.usage.outputTokens ?? 0),
totalTokens: cur.totalTokens + (data.usage.totalTokens ?? 0),
});
// 同步更新当前 Trace 步骤的 token 用量
getStore().updateLastTraceStep({
tokenUsage: {
promptTokens: data.usage.inputTokens ?? 0,
completionTokens: data.usage.outputTokens ?? 0,
totalTokens: data.usage.totalTokens ?? 0,
},
});
}
break;
@@ -180,6 +193,8 @@ export function useAgentStream(): void {
case 'done':
getStore().setStreaming(false);
getStore().setAgentStatus('idle');
// 标记最后一个 Trace 步骤为已完成
getStore().updateLastTraceStep({ completedAt: Date.now() });
getStore().saveTraceData();
break;
@@ -187,6 +202,7 @@ export function useAgentStream(): void {
case 'error':
getStore().setStreaming(false);
getStore().setAgentStatus('error');
getStore().updateLastTraceStep({ completedAt: Date.now() });
getStore().addMessage({
id: `msg_${Date.now()}_error`,
role: 'system',
@@ -194,17 +210,6 @@ export function useAgentStream(): void {
timestamp: Date.now(),
});
break;
// 状态变化
case 'state_change':
if (data.state) {
getStore().addTraceStep({
iteration: data.iteration ?? getStore().currentIteration,
state: data.state,
startedAt: Date.now(),
});
}
break;
}
});
@@ -215,16 +220,79 @@ export function useAgentStream(): void {
};
}, []);
// 监听状态变化事件(迭代号)
// ===== 状态变化事件(迭代追踪 + Trace 步骤 + 消息卡片) =====
useEffect(() => {
if (!window.metona?.agent?.onStateChange) return;
const unsubscribe = window.metona.agent.onStateChange((state: unknown) => {
const data = state as { sessionId?: string; iteration?: number; state?: string; previous?: string; current?: string };
if (data.iteration != null) {
const store = useAgentStore.getState();
if (data.iteration !== store.currentIteration) {
store.setCurrentIteration(data.iteration);
const data = state as {
sessionId?: string;
iteration?: number;
state?: string;
previous?: string;
current?: string;
};
const store = useAgentStore.getState();
// --- 迭代号更新 + 新消息卡片创建 ---
if (data.iteration != null && data.iteration !== store.currentIteration) {
const prevIteration = store.currentIteration;
store.setCurrentIteration(data.iteration);
// 迭代号增大 → 新一轮 ReAct 迭代开始,创建新的 assistant 消息卡片
// 仅当上一轮迭代已结束(prevIteration > 0)且上一条 assistant 消息有内容时
if (data.iteration > prevIteration && prevIteration > 0) {
const messages = store.messages;
const lastMsg = messages[messages.length - 1];
if (
lastMsg?.role === 'assistant' &&
(lastMsg.content || lastMsg.toolCalls?.length || lastMsg.reasoningContent)
) {
store.addMessage({
id: `msg_${Date.now()}_assistant`,
role: 'assistant',
content: '',
timestamp: Date.now(),
iteration: data.iteration,
});
}
}
}
// --- Trace 步骤管理 ---
if (data.state && data.iteration != null) {
const traceSteps = store.traceSteps;
// 标记上一个 Trace 步骤为已完成
if (traceSteps.length > 0) {
const lastStep = traceSteps[traceSteps.length - 1];
if (!lastStep.completedAt) {
store.updateLastTraceStep({ completedAt: Date.now() });
}
}
// 创建新的 Trace 步骤
store.addTraceStep({
id: `trace_${data.iteration}_${data.state}_${Date.now()}`,
iteration: data.iteration,
state: data.state,
startedAt: Date.now(),
});
// --- Agent 状态映射 ---
const stateToStatus: Record<string, AgentStatus> = {
THINKING: 'thinking',
EXECUTING: 'executing',
PARSING: 'thinking',
OBSERVING: 'thinking',
COMPRESSING: 'thinking',
INIT: 'thinking',
TERMINATED: 'idle',
};
const newStatus = stateToStatus[data.state];
if (newStatus) {
store.setAgentStatus(newStatus);
}
}
});
+18 -1
View File
@@ -56,6 +56,7 @@ export interface TokenUsage {
// ===== Trace 步骤 =====
export interface TraceStep {
id: string;
iteration: number;
state: string;
startedAt: number;
@@ -103,6 +104,7 @@ interface AgentState {
clearStreamingContent: () => void;
addTraceStep: (step: TraceStep) => void;
updateTraceStep: (iteration: number, updates: Partial<TraceStep>) => void;
updateLastTraceStep: (updates: Partial<TraceStep>) => void;
updateTokenUsage: (usage: Partial<TokenUsage>) => void;
setProvider: (provider: string, model: string) => void;
setMaxIterations: (max: number) => void;
@@ -160,7 +162,14 @@ export const useAgentStore = create<AgentState>((set, get) => ({
if (id && window.metona?.sessions?.getTrace) {
window.metona.sessions.getTrace(id).then((data) => {
if (data) {
if (data.traceSteps) set({ traceSteps: data.traceSteps as TraceStep[] });
if (data.traceSteps) {
// 兼容旧数据:为缺少 id 的 trace 步骤生成 id
const steps = (data.traceSteps as TraceStep[]).map((t, i) => ({
...t,
id: t.id ?? `trace_legacy_${t.iteration}_${t.state}_${i}`,
}));
set({ traceSteps: steps });
}
if (data.tokenUsage) set({ tokenUsage: data.tokenUsage as TokenUsage });
}
}).catch((err) => { console.error('[AgentStore]', err); });
@@ -321,6 +330,14 @@ export const useAgentStore = create<AgentState>((set, get) => ({
),
})),
updateLastTraceStep: (updates) =>
set((s) => {
if (s.traceSteps.length === 0) return s;
const steps = [...s.traceSteps];
steps[steps.length - 1] = { ...steps[steps.length - 1], ...updates };
return { traceSteps: steps };
}),
updateTokenUsage: (usage) =>
set((s) => ({ tokenUsage: { ...s.tokenUsage, ...usage } })),