fix: Agent Loop 多轮迭代消息互相覆盖 — 每轮创建独立消息气泡
问题:Agent Loop 多轮迭代(thought→action→observation→循环)时,每轮 onContent 回调都通过 updateLastAssistantMessage 替换同一个 placeholder 的 innerHTML,导致前一轮的回复内容被后一轮覆盖,只有最后一轮保留。 修复: - agent-engine.ts: AgentCallbacks 新增 onNewIteration 回调,每轮迭代 开始前通知 UI 层 - input-area.ts: sendMessageWithAgentLoop + handleRetry 添加 onNewIteration 处理——保存当前轮内容为独立消息、重置状态、创建新 placeholder。onDone 根据 agentModeIterations 判断是否需要重复保存
This commit is contained in:
@@ -291,10 +291,28 @@ async function handleRetry(): Promise<void> {
|
||||
});
|
||||
|
||||
try {
|
||||
let retryContent = '';
|
||||
let retryThinkContent = '';
|
||||
let retryIterations = 0;
|
||||
await runAgentLoop(userMsg.content || '', userMsg.images || [], historyMessages, {
|
||||
onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage('', thinking, null); },
|
||||
onContent: (content) => updateLastAssistantMessage(content, retryThinkContent || null, null),
|
||||
onNewIteration: () => {
|
||||
if (retryContent) {
|
||||
const prevMsg: ChatMessage = {
|
||||
role: 'assistant', content: retryContent, timestamp: Date.now(),
|
||||
...(retryThinkContent && { think: retryThinkContent }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||
...s, messages: [...s.messages, prevMsg], updatedAt: Date.now()
|
||||
}));
|
||||
renderMessages();
|
||||
}
|
||||
appendAssistantPlaceholder();
|
||||
retryContent = '';
|
||||
retryThinkContent = '';
|
||||
retryIterations++;
|
||||
},
|
||||
onThinking: (thinking) => { retryThinkContent = thinking; updateLastAssistantMessage(retryContent, thinking, null); },
|
||||
onContent: (content) => { retryContent = content; updateLastAssistantMessage(content, retryThinkContent || null, null); },
|
||||
onToolCallStart: (call) => {
|
||||
addToolCard({
|
||||
name: call.function.name, arguments: call.function.arguments,
|
||||
@@ -315,17 +333,34 @@ async function handleRetry(): Promise<void> {
|
||||
},
|
||||
onConfirmTool: async (call) => showToolConfirm(call),
|
||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||
const assistantMsg: ChatMessage = {
|
||||
role: 'assistant', content: finalContent || '', timestamp: Date.now(),
|
||||
...(retryThinkContent && { think: retryThinkContent }),
|
||||
...(toolRecords?.length && { toolCalls: toolRecords }),
|
||||
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||
...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now()
|
||||
}));
|
||||
updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null);
|
||||
retryContent = finalContent;
|
||||
if (retryIterations > 0) {
|
||||
updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null);
|
||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => {
|
||||
const msgs = [...s.messages];
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
if (msgs[i].role === 'assistant') {
|
||||
if (toolRecords?.length) msgs[i] = { ...msgs[i], toolCalls: toolRecords };
|
||||
if (loopStats?.eval_count) msgs[i] = { ...msgs[i], eval_count: loopStats.eval_count };
|
||||
if (loopStats?.total_duration) msgs[i] = { ...msgs[i], total_duration: loopStats.total_duration };
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { ...s, messages: msgs, updatedAt: Date.now() };
|
||||
});
|
||||
} else {
|
||||
const assistantMsg: ChatMessage = {
|
||||
role: 'assistant', content: finalContent || '', timestamp: Date.now(),
|
||||
...(retryThinkContent && { think: retryThinkContent }),
|
||||
...(toolRecords?.length && { toolCalls: toolRecords }),
|
||||
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||
...s, messages: [...s.messages, assistantMsg], updatedAt: Date.now()
|
||||
}));
|
||||
updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null);
|
||||
}
|
||||
renderMessages();
|
||||
await saveCurrentSession();
|
||||
updateTotalTokens();
|
||||
@@ -891,9 +926,32 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
|
||||
let assistantContent = '';
|
||||
let thinkContent = '';
|
||||
let agentModeIterations = 0; // 跟踪 onNewIteration 调用次数
|
||||
|
||||
try {
|
||||
await runAgentLoop(text || (pendingFiles.length > 0 ? `请分析 ${pendingFiles.map(f => f.name).join(', ')}` : ''), images, historyMessages, {
|
||||
onNewIteration: () => {
|
||||
// Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder
|
||||
if (assistantContent) {
|
||||
const prevMsg: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantContent,
|
||||
timestamp: Date.now(),
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
...session,
|
||||
messages: [...session.messages, prevMsg],
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
renderMessages();
|
||||
}
|
||||
// 创建新 placeholder,重置内容
|
||||
appendAssistantPlaceholder();
|
||||
assistantContent = '';
|
||||
thinkContent = '';
|
||||
agentModeIterations++;
|
||||
},
|
||||
onThinking: (thinking) => {
|
||||
thinkContent = thinking;
|
||||
updateLastAssistantMessage(assistantContent, thinkContent || null, null);
|
||||
@@ -926,22 +984,40 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||
assistantContent = finalContent;
|
||||
|
||||
const assistantMsg: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantContent || '',
|
||||
timestamp: Date.now(),
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
...(toolRecords?.length && { toolCalls: toolRecords }),
|
||||
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
...session,
|
||||
messages: [...session.messages, assistantMsg],
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
|
||||
updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null);
|
||||
// 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里只更新 placeholder
|
||||
if (agentModeIterations > 0) {
|
||||
updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null);
|
||||
// 更新最后一条消息的工具记录(onNewIteration 保存时没有 toolCalls)
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => {
|
||||
const msgs = [...session.messages];
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
if (msgs[i].role === 'assistant') {
|
||||
if (toolRecords?.length) msgs[i] = { ...msgs[i], toolCalls: toolRecords };
|
||||
if (loopStats?.eval_count) msgs[i] = { ...msgs[i], eval_count: loopStats.eval_count };
|
||||
if (loopStats?.total_duration) msgs[i] = { ...msgs[i], total_duration: loopStats.total_duration };
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { ...session, messages: msgs, updatedAt: Date.now() };
|
||||
});
|
||||
} else {
|
||||
// 单迭代模式:正常保存
|
||||
const assistantMsg: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantContent || '',
|
||||
timestamp: Date.now(),
|
||||
...(thinkContent && { think: thinkContent }),
|
||||
...(toolRecords?.length && { toolCalls: toolRecords }),
|
||||
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
||||
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
|
||||
};
|
||||
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||
...session,
|
||||
messages: [...session.messages, assistantMsg],
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null);
|
||||
}
|
||||
renderMessages();
|
||||
await saveCurrentSession();
|
||||
updateTotalTokens();
|
||||
|
||||
@@ -357,6 +357,8 @@ export interface AgentCallbacks {
|
||||
onToolCallError: (name: string, error: string, call: ToolCall) => void;
|
||||
onDone: (finalContent: string, toolRecords?: ToolCallRecord[], stats?: { eval_count?: number; total_duration?: number }) => void;
|
||||
onConfirmTool: (call: ToolCall) => Promise<boolean>;
|
||||
/** Agent Loop 新迭代开始(前一轮工具执行完毕,下一轮流式输出即将开始) */
|
||||
onNewIteration?: () => void;
|
||||
}
|
||||
|
||||
/** 保存执行轨迹到 SQLite */
|
||||
@@ -530,6 +532,11 @@ export async function runAgentLoop(
|
||||
throw new DOMException('Aborted', 'AbortError');
|
||||
}
|
||||
|
||||
// 非首轮迭代:通知 UI 创建新的消息气泡(防止每轮内容互相覆盖)
|
||||
if (loopCount > 1 && callbacks.onNewIteration) {
|
||||
callbacks.onNewIteration();
|
||||
}
|
||||
|
||||
logAgentLoop(loopCount, maxLoops);
|
||||
|
||||
let thinking = '';
|
||||
|
||||
Reference in New Issue
Block a user