fix: Agent Loop 多轮迭代消息保存和 token 统计
- 修复 onDone 多轮迭代时更新旧消息而非创建新消息,导致 post-tool 回复丢失 - 修复 pre-tool 消息的 toolCalls 未保存(onNewIteration 未传递 toolCalls) - 修复最后两条 AI 消息无 token 显示(统计附着到被覆盖的消息上) - onNewIteration 回调增加 toolCalls 参数,保存消息时附带工具调用记录 - handleRetry 同步修复
This commit is contained in:
@@ -295,11 +295,17 @@ async function handleRetry(): Promise<void> {
|
|||||||
let retryThinkContent = '';
|
let retryThinkContent = '';
|
||||||
let retryIterations = 0;
|
let retryIterations = 0;
|
||||||
await runAgentLoop(userMsg.content || '', userMsg.images || [], historyMessages, {
|
await runAgentLoop(userMsg.content || '', userMsg.images || [], historyMessages, {
|
||||||
onNewIteration: () => {
|
onNewIteration: (toolCalls) => {
|
||||||
if (retryContent) {
|
if (retryContent || toolCalls) {
|
||||||
|
const now = Date.now();
|
||||||
|
const toolCallRecords: ToolCallRecord[] | undefined = toolCalls?.map(tc => ({
|
||||||
|
name: tc.function.name, arguments: tc.function.arguments,
|
||||||
|
result: null, status: 'running' as const, timestamp: now
|
||||||
|
}));
|
||||||
const prevMsg: ChatMessage = {
|
const prevMsg: ChatMessage = {
|
||||||
role: 'assistant', content: retryContent, timestamp: Date.now(),
|
role: 'assistant', content: retryContent || '', timestamp: now,
|
||||||
...(retryThinkContent && { think: retryThinkContent }),
|
...(retryThinkContent && { think: retryThinkContent }),
|
||||||
|
...(toolCallRecords?.length && { toolCalls: toolCallRecords }),
|
||||||
};
|
};
|
||||||
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
state.update(KEYS.CURRENT_SESSION, (s: ChatSession | null) => ({
|
||||||
...s, messages: [...s.messages, prevMsg], updatedAt: Date.now()
|
...s, messages: [...s.messages, prevMsg], updatedAt: Date.now()
|
||||||
@@ -335,19 +341,18 @@ async function handleRetry(): Promise<void> {
|
|||||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||||
retryContent = finalContent;
|
retryContent = finalContent;
|
||||||
if (retryIterations > 0) {
|
if (retryIterations > 0) {
|
||||||
|
if (finalContent) {
|
||||||
|
const lastMsg: ChatMessage = {
|
||||||
|
role: 'assistant', content: finalContent, timestamp: Date.now(),
|
||||||
|
...(retryThinkContent && { think: retryThinkContent }),
|
||||||
|
...(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, lastMsg], updatedAt: Date.now()
|
||||||
|
}));
|
||||||
|
}
|
||||||
updateLastAssistantMessage(finalContent, retryThinkContent || null, loopStats || null);
|
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 {
|
} else {
|
||||||
const assistantMsg: ChatMessage = {
|
const assistantMsg: ChatMessage = {
|
||||||
role: 'assistant', content: finalContent || '', timestamp: Date.now(),
|
role: 'assistant', content: finalContent || '', timestamp: Date.now(),
|
||||||
@@ -933,24 +938,34 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await runAgentLoop(text || (pendingFiles.length > 0 ? `请分析 ${pendingFiles.map(f => f.name).join(', ')}` : ''), images, historyMessages, {
|
await runAgentLoop(text || (pendingFiles.length > 0 ? `请分析 ${pendingFiles.map(f => f.name).join(', ')}` : ''), images, historyMessages, {
|
||||||
onNewIteration: () => {
|
onNewIteration: (toolCalls) => {
|
||||||
// Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder
|
// Agent Loop 新迭代开始:保存上一轮内容为独立消息,创建新 placeholder
|
||||||
// 先移除旧 placeholder(防止被 renderMessages 当作已渲染消息计数)
|
// 先移除旧 placeholder(防止被 renderMessages 当作已渲染消息计数)
|
||||||
const oldPlaceholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
|
const oldPlaceholder = document.querySelector('#messagesContainer .message.assistant.loading') as HTMLElement;
|
||||||
if (oldPlaceholder) oldPlaceholder.remove();
|
if (oldPlaceholder) oldPlaceholder.remove();
|
||||||
|
|
||||||
if (assistantContent) {
|
// 有文本内容 或 有工具调用 → 保存为独立消息
|
||||||
|
if (assistantContent || toolCalls) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// 计算本轮迭代的 token 和时长
|
// 计算本轮迭代的 token 和时长
|
||||||
const currentEvalCount = state.get<number>('_currentEvalCount', 0);
|
const currentEvalCount = state.get<number>('_currentEvalCount', 0);
|
||||||
const iterationEvalCount = Math.max(0, currentEvalCount - lastIterationEvalCount);
|
const iterationEvalCount = Math.max(0, currentEvalCount - lastIterationEvalCount);
|
||||||
const iterationDuration = (now - lastIterationStartTime) * 1e6;
|
const iterationDuration = (now - lastIterationStartTime) * 1e6;
|
||||||
|
|
||||||
|
const toolCallRecords: ToolCallRecord[] | undefined = toolCalls?.map(tc => ({
|
||||||
|
name: tc.function.name,
|
||||||
|
arguments: tc.function.arguments,
|
||||||
|
result: null,
|
||||||
|
status: 'running' as const,
|
||||||
|
timestamp: now
|
||||||
|
}));
|
||||||
|
|
||||||
const prevMsg: ChatMessage = {
|
const prevMsg: ChatMessage = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: assistantContent,
|
content: assistantContent || '',
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
...(thinkContent && { think: thinkContent }),
|
...(thinkContent && { think: thinkContent }),
|
||||||
|
...(toolCallRecords?.length && { toolCalls: toolCallRecords }),
|
||||||
...(iterationEvalCount > 0 && { eval_count: iterationEvalCount }),
|
...(iterationEvalCount > 0 && { eval_count: iterationEvalCount }),
|
||||||
...(iterationDuration > 0 && { total_duration: iterationDuration }),
|
...(iterationDuration > 0 && { total_duration: iterationDuration }),
|
||||||
};
|
};
|
||||||
@@ -1002,31 +1017,34 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
|||||||
onDone: async (finalContent, toolRecords, loopStats) => {
|
onDone: async (finalContent, toolRecords, loopStats) => {
|
||||||
assistantContent = finalContent;
|
assistantContent = finalContent;
|
||||||
|
|
||||||
// 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里只更新 placeholder
|
// 如果有多个迭代,最后一条消息已由 onNewIteration 保存,这里创建新消息保存最终回复
|
||||||
if (agentModeIterations > 0) {
|
if (agentModeIterations > 0) {
|
||||||
// 最后一轮迭代的 per-iteration stats(保证每条消息统计一致,不重复计数)
|
if (assistantContent) {
|
||||||
const finalEvalCount = loopStats?.eval_count || 0;
|
// 最后一轮迭代的 stats(loopStats.eval_count 即为最后一轮的 token 数)
|
||||||
const lastIterTokens = Math.max(0, finalEvalCount - lastIterationEvalCount);
|
const lastIterDuration = (Date.now() - lastIterationStartTime) * 1e6;
|
||||||
const lastIterDuration = (Date.now() - lastIterationStartTime) * 1e6;
|
|
||||||
const perIterStats = {
|
|
||||||
eval_count: lastIterTokens > 0 ? lastIterTokens : undefined,
|
|
||||||
total_duration: lastIterDuration > 0 ? lastIterDuration : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const lastMsg: ChatMessage = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: assistantContent,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
...(thinkContent && { think: thinkContent }),
|
||||||
|
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
|
||||||
|
...(lastIterDuration > 0 && { total_duration: lastIterDuration }),
|
||||||
|
};
|
||||||
|
state.update(KEYS.CURRENT_SESSION, (session: ChatSession | null) => ({
|
||||||
|
...session,
|
||||||
|
messages: [...session.messages, lastMsg],
|
||||||
|
updatedAt: Date.now()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const perIterStats = {
|
||||||
|
eval_count: loopStats?.eval_count,
|
||||||
|
total_duration: (() => {
|
||||||
|
const dur = (Date.now() - lastIterationStartTime) * 1e6;
|
||||||
|
return dur > 0 ? dur : undefined;
|
||||||
|
})(),
|
||||||
|
};
|
||||||
updateLastAssistantMessage(assistantContent, thinkContent || null, perIterStats);
|
updateLastAssistantMessage(assistantContent, thinkContent || null, perIterStats);
|
||||||
// 更新最后一条消息的工具记录和 per-iteration stats(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 (lastIterTokens > 0) msgs[i] = { ...msgs[i], eval_count: lastIterTokens };
|
|
||||||
if (lastIterDuration > 0) msgs[i] = { ...msgs[i], total_duration: lastIterDuration };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ...session, messages: msgs, updatedAt: Date.now() };
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// 单迭代模式:正常保存
|
// 单迭代模式:正常保存
|
||||||
const assistantMsg: ChatMessage = {
|
const assistantMsg: ChatMessage = {
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ export interface AgentCallbacks {
|
|||||||
onDone: (finalContent: string, toolRecords?: ToolCallRecord[], stats?: { eval_count?: number; total_duration?: number }) => void;
|
onDone: (finalContent: string, toolRecords?: ToolCallRecord[], stats?: { eval_count?: number; total_duration?: number }) => void;
|
||||||
onConfirmTool: (call: ToolCall) => Promise<boolean>;
|
onConfirmTool: (call: ToolCall) => Promise<boolean>;
|
||||||
/** Agent Loop 新迭代开始(前一轮工具执行完毕,下一轮流式输出即将开始) */
|
/** Agent Loop 新迭代开始(前一轮工具执行完毕,下一轮流式输出即将开始) */
|
||||||
onNewIteration?: () => void;
|
onNewIteration?: (toolCalls?: ToolCall[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存执行轨迹到 SQLite */
|
/** 保存执行轨迹到 SQLite */
|
||||||
@@ -535,7 +535,7 @@ export async function runAgentLoop(
|
|||||||
|
|
||||||
// 非首轮迭代:通知 UI 创建新的消息气泡(防止每轮内容互相覆盖)
|
// 非首轮迭代:通知 UI 创建新的消息气泡(防止每轮内容互相覆盖)
|
||||||
if (loopCount > 1 && callbacks.onNewIteration) {
|
if (loopCount > 1 && callbacks.onNewIteration) {
|
||||||
callbacks.onNewIteration();
|
callbacks.onNewIteration(toolCalls.length > 0 ? toolCalls : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
logAgentLoop(loopCount, maxLoops);
|
logAgentLoop(loopCount, maxLoops);
|
||||||
|
|||||||
Reference in New Issue
Block a user