fix: preserve investigation execution history by round

This commit is contained in:
2026-10-30 17:07:53 +08:00
parent 1ee21d1c7f
commit 185bf31446
16 changed files with 830 additions and 32 deletions
@@ -27,6 +27,7 @@ export type AgentSessionRuntime = {
topicId?: string | null;
purpose: AgentSessionPurpose;
taskId?: string;
taskExecutionHistory?: { taskId: string; runId: string; sessionId: string };
draft: string;
reference?: AnswerUiSelection;
taskDiscussion?: { context: AgentTaskDiscussionContext; reportGeneration?: number | null };
@@ -200,7 +200,12 @@ export function useWorkbenchAgent({
}
}
}, [resources, scopeKey, scopeClient, selectedModel, approvalMode]);
const interactionRequests = useInteractionRequests(scopeClient, scopeKey, sessionId, continueInteraction);
const interactionRequests = useInteractionRequests(
scopeClient,
scopeKey,
sessionPurpose === "task_execution" ? null : sessionId,
continueInteraction
);
const refreshInteractionRequests = interactionRequests.refresh;
@@ -510,7 +515,9 @@ export function useWorkbenchAgent({
const streaming = runtimeRunning(runtime);
const { data: pendingInputs = [], mutate: refreshPendingInputs } = useSWR(
sessionId ? ["agent", "response-inputs", getAccessToken, scopeKey, sessionId] : null,
sessionId && sessionPurpose !== "task_execution"
? ["agent", "response-inputs", getAccessToken, scopeKey, sessionId]
: null,
() => clientRef.current.listResponseInputs(sessionId!),
{ refreshInterval: streaming ? 1000 : 0, shouldRetryOnError: false }
);
@@ -776,6 +783,118 @@ export function useWorkbenchAgent({
return id;
}, [activateRuntime, getAccessToken, getProjectId, projectScopeKey, resources]);
const openTaskExecutionHistory = useCallback(async (taskId: string, runId: string, executionSessionId: string) => {
const normalizedTaskId = taskId.trim();
const normalizedRunId = runId.trim();
const normalizedSessionId = executionSessionId.trim();
if (!normalizedTaskId || !normalizedRunId || !normalizedSessionId) {
throw new Error("调查任务缺少可用的执行记录标识。");
}
const projectClient = createAgentApiClient(undefined, {
getAccessToken: getWorkbenchTokenProvider(getAccessToken),
getProjectId
});
const loaded = await projectClient.loadTaskExecutionSession(
normalizedTaskId,
normalizedRunId,
normalizedSessionId,
AbortSignal.timeout(15_000)
);
if (!loaded) throw new Error("调查执行记录不存在或尚未持久化。");
let target = agentRuntimes(resources).find((entry) =>
entry.scope === projectScopeKey && entry.sessionId === loaded.sessionId
);
if (!target) {
target = createAgentRuntime(
resources,
projectScopeKey,
projectClient,
createWorkbenchAgentTransport(projectClient, getAccessToken, getProjectId),
loaded
);
} else {
target.client = projectClient;
target.chat.messages = toAgentUiMessages(loaded.messages);
target.title = loaded.title;
target.purpose = "task_execution";
target.taskId = normalizedTaskId;
target.loadedAt = Date.now();
target.updatedAt = loaded.updatedAt;
target.runStatus = loaded.runStatus;
target.error = undefined;
}
target.taskDiscussion = undefined;
target.taskExecutionHistory = {
taskId: normalizedTaskId,
runId: normalizedRunId,
sessionId: normalizedSessionId
};
target.draft = "";
activateRuntime(target);
setStatusLabel(loaded.runStatus === "running" ? "调查执行记录自动同步中" : "调查执行记录已打开");
return loaded.sessionId;
}, [activateRuntime, getAccessToken, getProjectId, projectScopeKey, resources]);
useEffect(() => {
const target = runtimeRef.current;
const binding = target?.taskExecutionHistory;
if (sessionPurpose !== "task_execution" || !target || !binding || target.sessionId !== binding.sessionId) return;
let stopped = false;
let refreshing = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const controller = new AbortController();
const schedule = (delay = 2_000) => {
if (!stopped) timer = setTimeout(() => void refresh(), delay);
};
const refresh = async () => {
if (stopped || refreshing || resources.activeAgent !== target || target.purpose !== "task_execution") return;
refreshing = true;
if (timer) {
clearTimeout(timer);
timer = undefined;
}
try {
const loaded = await target.client.loadTaskExecutionSession(
binding.taskId,
binding.runId,
binding.sessionId,
controller.signal
);
if (!loaded || stopped || resources.activeAgent !== target || target.purpose !== "task_execution") {
refreshing = false;
return;
}
target.chat.messages = mergeHistoryMessages(target.chat.messages, toAgentUiMessages(loaded.messages));
target.title = loaded.title;
target.updatedAt = loaded.updatedAt;
target.loadedAt = Date.now();
target.runStatus = loaded.runStatus;
target.connectionError = undefined;
setSessionTitle(loaded.title);
setStatusLabel(loaded.runStatus === "running" ? "调查执行记录自动同步中" : "调查执行记录已同步");
resources.notify();
refreshing = false;
if (loaded.runStatus === "running" || loaded.isStreaming) schedule();
} catch (error) {
refreshing = false;
if (controller.signal.aborted || stopped) return;
target.connectionError = error instanceof Error ? error.message : "调查执行记录同步失败";
setStatusLabel("调查执行记录同步中断,正在重试");
resources.notify();
schedule(4_000);
}
};
const refreshOnFocus = () => void refresh();
window.addEventListener("focus", refreshOnFocus);
schedule();
return () => {
stopped = true;
controller.abort();
if (timer) clearTimeout(timer);
window.removeEventListener("focus", refreshOnFocus);
};
}, [resources, sessionId, sessionPurpose]);
const renameHistorySession = useCallback(
async (nextSessionId: string, title: string) => {
const normalizedTitle = title.trim();
@@ -890,6 +1009,42 @@ export function useWorkbenchAgent({
if (!normalizedPrompt) {
return;
}
let activeSessionId = sessionIdRef.current;
if (runtimeRef.current?.purpose === "task_execution") {
const target = runtimeRef.current;
const binding = target.taskExecutionHistory;
if (!binding) throw new Error("调查执行记录缺少调试分支信息,请重新打开后再试。");
setStatusLabel("正在创建调查调试分支");
try {
const branch = await target.client.createTaskExecutionDebugSession(
binding.taskId,
binding.runId,
binding.sessionId
);
const loaded = await target.client.loadSession(branch.session_id, {
messageLimit: 40,
signal: AbortSignal.timeout(15_000)
});
if (!loaded) throw new Error("调查调试分支创建后无法读取。");
if (runtimeRef.current !== target) throw new Error("当前 Agent 会话已切换,请在新会话中重新发送。");
target.sessionId = loaded.sessionId;
target.title = loaded.title;
target.purpose = "task_collaboration";
target.taskId = binding.taskId;
target.taskExecutionHistory = undefined;
target.chat.messages = mergeHistoryMessages(target.chat.messages, toAgentUiMessages(loaded.messages));
target.messagePage = loaded.messagePage;
target.loadedAt = Date.now();
target.updatedAt = loaded.updatedAt;
target.runStatus = loaded.runStatus;
activeSessionId = loaded.sessionId;
activateRuntime(target);
setStatusLabel("调查调试会话已创建");
} catch (error) {
showMapNotice({ tone: "error", title: "调查调试会话创建失败", message: error instanceof Error ? error.message : "请稍后重试。" });
throw error;
}
}
if (streaming || interactionRequests.awaitingInput) {
const owningRuntime = runtimeRef.current;
@@ -925,8 +1080,8 @@ export function useWorkbenchAgent({
{ text: normalizedPrompt },
{
body: {
session_id: sessionIdRef.current ?? undefined,
topic_id: !sessionIdRef.current ? runtimeRef.current?.topicId ?? undefined : undefined,
session_id: activeSessionId ?? undefined,
topic_id: !activeSessionId ? runtimeRef.current?.topicId ?? undefined : undefined,
model: selectedModel || undefined,
approval_mode: approvalMode,
capabilities: frontendActionEnabledRef.current ? ["frontend-action@1", "host-question@1"] : ["host-question@1"],
@@ -935,7 +1090,7 @@ export function useWorkbenchAgent({
}
);
},
[approvalMode, chat, inputDelivery, interactionRequests.awaitingInput, refreshInteractionRequests, refreshPendingInputs, selectedModel, streaming, taskDiscussion]
[activateRuntime, approvalMode, chat, inputDelivery, interactionRequests.awaitingInput, refreshInteractionRequests, refreshPendingInputs, selectedModel, streaming, taskDiscussion]
);
const triggerEvent = useCallback(async (trigger: AgentEventTriggerContext, prompt: string) => {
@@ -1060,6 +1215,7 @@ export function useWorkbenchAgent({
startNewSession,
loadHistorySession,
openTaskCollaboration,
openTaskExecutionHistory,
renameHistorySession,
deleteHistorySession,
sessionHistory,