fix: preserve investigation execution history by round
This commit is contained in:
@@ -138,6 +138,76 @@ describe("Agent API client sessions", () => {
|
||||
await expect(createAgentApiClient("http://agent.local").loadSession("missing")).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("loads a task execution history through its exact task and run binding", async () => {
|
||||
const fetchMock = vi.fn(async (..._args: Parameters<typeof fetch>) => Response.json({
|
||||
id: "execution-session",
|
||||
session_id: "execution-session",
|
||||
title: "南桥管道爆管调查 · 执行记录",
|
||||
purpose: "task_execution",
|
||||
task_id: "50000000-0000-4000-8000-000000000001",
|
||||
created_at: "2026-10-30T01:00:00Z",
|
||||
updated_at: "2026-10-30T01:05:00Z",
|
||||
messages: [{ id: "assistant", role: "assistant", parts: [{ type: "text", text: "调查完成" }] }],
|
||||
is_streaming: false,
|
||||
run_status: "completed"
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const client = createAgentApiClient("http://agent.local", {
|
||||
getAccessToken: async () => "token",
|
||||
getProjectId: async () => "project"
|
||||
});
|
||||
|
||||
const loaded = await client.loadTaskExecutionSession(
|
||||
"50000000-0000-4000-8000-000000000001",
|
||||
"70000000-0000-4000-8000-000000000001",
|
||||
"execution-session"
|
||||
);
|
||||
|
||||
expect(loaded).toMatchObject({
|
||||
sessionId: "execution-session",
|
||||
title: "南桥管道爆管调查 · 执行记录",
|
||||
purpose: "task_execution",
|
||||
taskId: "50000000-0000-4000-8000-000000000001",
|
||||
runStatus: "completed"
|
||||
});
|
||||
const url = new URL(String(fetchMock.mock.calls[0]?.[0]));
|
||||
expect(url.pathname).toBe("/api/v1/task/50000000-0000-4000-8000-000000000001/execution-session");
|
||||
expect(url.searchParams.get("run_id")).toBe("70000000-0000-4000-8000-000000000001");
|
||||
expect(url.searchParams.get("session_id")).toBe("execution-session");
|
||||
const headers = new Headers(fetchMock.mock.calls[0]?.[1]?.headers);
|
||||
expect(headers.get("Authorization")).toBe("Bearer token");
|
||||
expect(headers.get("X-Project-Id")).toBe("project");
|
||||
});
|
||||
|
||||
it("creates a debug branch bound to an exact task execution history", async () => {
|
||||
const fetchMock = vi.fn(async (..._args: Parameters<typeof fetch>) => Response.json({
|
||||
session_id: "debug-session",
|
||||
title: "南桥管道爆管调查 · 调试",
|
||||
purpose: "task_collaboration",
|
||||
task_id: "50000000-0000-4000-8000-000000000001",
|
||||
parent_session_id: "execution-session",
|
||||
created: true
|
||||
}, { status: 201 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const client = createAgentApiClient("http://agent.local", {
|
||||
getAccessToken: async () => "token",
|
||||
getProjectId: async () => "project"
|
||||
});
|
||||
|
||||
await expect(client.createTaskExecutionDebugSession(
|
||||
"50000000-0000-4000-8000-000000000001",
|
||||
"70000000-0000-4000-8000-000000000001",
|
||||
"execution-session"
|
||||
)).resolves.toMatchObject({ session_id: "debug-session", purpose: "task_collaboration" });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] ?? [];
|
||||
expect(new URL(String(url)).pathname).toBe("/api/v1/task/50000000-0000-4000-8000-000000000001/execution-session/debug");
|
||||
expect(JSON.parse(String(init?.body))).toEqual({
|
||||
run_id: "70000000-0000-4000-8000-000000000001",
|
||||
session_id: "execution-session"
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back when a candidate is not the agent service", async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
|
||||
@@ -174,6 +174,8 @@ export type AgentApiClient = InteractionRequestApi & {
|
||||
moveSessionToTopic: (sessionId: string, topicId: string | null) => Promise<void>;
|
||||
loadOlderMessages: (sessionId: string, before: string, signal?: AbortSignal) => Promise<AgentHistoryMessagePage>;
|
||||
createTaskCollaborationSession: (taskId: string) => Promise<AgentChatSession>;
|
||||
createTaskExecutionDebugSession: (taskId: string, runId: string, sessionId: string) => Promise<AgentChatSession>;
|
||||
loadTaskExecutionSession: (taskId: string, runId: string, sessionId: string, signal?: AbortSignal) => Promise<AgentLoadedChatSession | null>;
|
||||
loadSession: (sessionId: string, options?: { messageLimit?: number; signal?: AbortSignal }) => Promise<AgentLoadedChatSession | null>;
|
||||
streamSession: (
|
||||
sessionId: string,
|
||||
@@ -365,6 +367,30 @@ export function createAgentApiClient(
|
||||
);
|
||||
},
|
||||
|
||||
async createTaskExecutionDebugSession(taskId, runId, sessionId) {
|
||||
return requestJson<AgentChatSession>(
|
||||
`/task/${encodeURIComponent(taskId)}/execution-session/debug`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ run_id: runId, session_id: sessionId })
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async loadTaskExecutionSession(taskId, runId, sessionId, signal) {
|
||||
const query = new URLSearchParams({ run_id: runId, session_id: sessionId });
|
||||
const response = await request(
|
||||
`/task/${encodeURIComponent(taskId)}/execution-session?${query}`,
|
||||
{ signal }
|
||||
);
|
||||
const text = await response.text();
|
||||
const data = text ? JSON.parse(text) : null;
|
||||
if (response.status === 404) return null;
|
||||
if (!response.ok) throw new Error(getResponseErrorMessage(data, response.status));
|
||||
return toLoadedSession(data);
|
||||
},
|
||||
|
||||
async getFrontendActionRegistry() {
|
||||
return requestJson<unknown>("/frontend-action-registry");
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
Eye,
|
||||
History,
|
||||
LoaderCircle,
|
||||
Pause,
|
||||
@@ -153,6 +154,7 @@ type AgentCommandPanelProps = {
|
||||
onRetryPromptSuggestions?: () => void;
|
||||
activeSessionId?: string | null;
|
||||
qaOnly?: boolean;
|
||||
executionHistory?: boolean;
|
||||
statusLabel?: string;
|
||||
connectionStatus?: AgentConnectionStatus;
|
||||
streaming?: boolean;
|
||||
@@ -220,6 +222,7 @@ export function AgentCommandPanel({
|
||||
onRetryPromptSuggestions,
|
||||
activeSessionId,
|
||||
qaOnly = false,
|
||||
executionHistory = false,
|
||||
connectionStatus = "connecting",
|
||||
streaming = false,
|
||||
inputDelivery = "steer",
|
||||
@@ -561,6 +564,7 @@ export function AgentCommandPanel({
|
||||
onResumeSpeech={resumeSpeech}
|
||||
onStopSpeech={stopSpeech}
|
||||
qaOnly={qaOnly}
|
||||
showProcess={!qaOnly || executionHistory}
|
||||
onLocateAnswerFeatures={onLocateAnswerFeatures}
|
||||
onSubmitAnswerForm={onSubmitPrompt}
|
||||
onAsk={(selection) => {
|
||||
@@ -603,6 +607,15 @@ export function AgentCommandPanel({
|
||||
: "border-t border-[var(--surface-divider)] p-3"
|
||||
)}
|
||||
>
|
||||
{executionHistory ? (
|
||||
<div role="note" className="agent-panel-control mb-1 flex min-h-14 items-center gap-2.5 rounded-xl px-3 py-2 text-sm">
|
||||
<Eye className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-foreground">执行记录自动同步</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">发送消息后进入独立调试会话,系统执行记录不会被修改。</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{centeredComposer && !promptReference ? (
|
||||
<ConversationEmptyState className="agent-chat-empty-state">
|
||||
<h2 className="text-2xl font-medium tracking-tight text-foreground">有什么可以帮你?</h2>
|
||||
@@ -1265,6 +1278,7 @@ type BackendMessageListProps = {
|
||||
onResumeSpeech: () => void;
|
||||
onStopSpeech: () => void;
|
||||
qaOnly: boolean;
|
||||
showProcess: boolean;
|
||||
onLocateAnswerFeatures?: (target: AnswerUiMapTarget) => Promise<unknown> | void;
|
||||
onSubmitAnswerForm: (text: string) => Promise<void> | void;
|
||||
onAsk?: (selection: AnswerUiSelection) => void;
|
||||
@@ -1329,6 +1343,7 @@ function BackendMessage({
|
||||
onResumeSpeech,
|
||||
onStopSpeech,
|
||||
qaOnly,
|
||||
showProcess,
|
||||
onLocateAnswerFeatures,
|
||||
onSubmitAnswerForm,
|
||||
onAsk,
|
||||
@@ -1366,10 +1381,9 @@ function BackendMessage({
|
||||
processMessages.some(
|
||||
(source) =>
|
||||
getAnswerPresentation(source).process.length > 0 ||
|
||||
(!qaOnly &&
|
||||
(source.progress?.length ||
|
||||
(!hasPendingMessageInput(source) &&
|
||||
(source.todos?.todos.length || source.permissions?.length || source.questions?.length)))),
|
||||
(showProcess && source.progress?.length) ||
|
||||
(!qaOnly && !hasPendingMessageInput(source)
|
||||
&& (source.todos?.todos.length || source.permissions?.length || source.questions?.length)),
|
||||
);
|
||||
const hasProcessError = processMessages.some((source) => source.progress?.some((item) => item.status === "error"));
|
||||
return (
|
||||
@@ -1485,6 +1499,7 @@ function BackendMessage({
|
||||
<AgentMessageProcessContent
|
||||
messages={processMessages}
|
||||
qaOnly={qaOnly}
|
||||
showProgress={showProcess}
|
||||
runningMessageId={!completed ? message.id : undefined}
|
||||
/>
|
||||
</AgentProcessDisclosure>
|
||||
|
||||
@@ -7,10 +7,12 @@ import { AgentMessageDetails, AgentMessageProgress } from "./agent-message-detai
|
||||
export function AgentMessageProcessContent({
|
||||
messages,
|
||||
qaOnly,
|
||||
showProgress,
|
||||
runningMessageId
|
||||
}: {
|
||||
messages: AgentChatMessage[];
|
||||
qaOnly: boolean;
|
||||
showProgress: boolean;
|
||||
runningMessageId?: string;
|
||||
}) {
|
||||
const progress = messages.flatMap((message) => (message.progress ?? []).map((item) => ({
|
||||
@@ -18,7 +20,7 @@ export function AgentMessageProcessContent({
|
||||
})));
|
||||
return (
|
||||
<>
|
||||
{!qaOnly ? <AgentMessageProgress progress={progress} running={Boolean(runningMessageId)} /> : null}
|
||||
{showProgress ? <AgentMessageProgress progress={progress} running={Boolean(runningMessageId)} /> : null}
|
||||
{messages.map((message) => {
|
||||
const { process } = getAnswerPresentation(message);
|
||||
const running = message.id === runningMessageId;
|
||||
|
||||
Reference in New Issue
Block a user