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:
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 } })),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user