fix(workbench): leave completed no-final response empty
This commit is contained in:
@@ -279,6 +279,7 @@ function createScenarioState(scenarioId: string): ScenarioState {
|
||||
traces.trc_terminal_empty = terminalEmptyTrace();
|
||||
}
|
||||
if (id === "progress-only-final-response") markRunningProgressOnly(sessions, traces);
|
||||
if (id === "terminal-completed-no-final-response") markRunningNoFinalResponse(sessions, traces);
|
||||
if (id === "scroll-follow-long-trace") {
|
||||
const session = scrollFollowSession();
|
||||
sessions.unshift(session);
|
||||
@@ -301,7 +302,7 @@ function createScenarioState(scenarioId: string): ScenarioState {
|
||||
? "ses_completed"
|
||||
: id === "terminal-empty-trace"
|
||||
? "ses_terminal_empty"
|
||||
: id === "progress-only-final-response"
|
||||
: id === "progress-only-final-response" || id === "terminal-completed-no-final-response"
|
||||
? "ses_running"
|
||||
: base.selectedSessionId;
|
||||
const staleTraceId = id === "stale-nested-trace" || id === "stale-submit-restore" ? "trc_stale_502" : null;
|
||||
@@ -318,7 +319,7 @@ function createScenarioState(scenarioId: string): ScenarioState {
|
||||
sessionDelayMs: id === "loading" ? 2_500 : 0,
|
||||
sessionDetailDelayMs: id === "legacy-cnv-deeplink-canonical" ? 1_500 : id === "session-switch-delayed-detail-frame" ? 900 : 0,
|
||||
chatDelayMs: id === "submit-authority-race" ? 1_000 : 0,
|
||||
terminalScript: id === "event-replay" || id === "running-to-terminal" || id === "stale-submit-restore" || id === "progress-only-final-response",
|
||||
terminalScript: id === "event-replay" || id === "running-to-terminal" || id === "stale-submit-restore" || id === "progress-only-final-response" || id === "terminal-completed-no-final-response",
|
||||
terminalFailureScript: id === "stale-submit-restore",
|
||||
staleTraceId,
|
||||
liveBackfillTraceId: null,
|
||||
@@ -354,6 +355,45 @@ function markRunningProgressOnly(sessions: SessionRecord[], traces: Record<strin
|
||||
});
|
||||
}
|
||||
|
||||
function markRunningNoFinalResponse(sessions: SessionRecord[], traces: Record<string, JsonRecord>): void {
|
||||
const trace = noFinalRunningTrace();
|
||||
traces.trc_running = trace;
|
||||
const session = sessions.find((item) => item.sessionId === "ses_running");
|
||||
if (!session) return;
|
||||
session.status = "running";
|
||||
session.lastTraceId = "trc_running";
|
||||
session.firstUserMessagePreview = "completed turn without final response";
|
||||
session.messages = (session.messages ?? []).map((message) => {
|
||||
if (message.role === "user") return { ...message, text: "completed turn without final response" };
|
||||
if (message.role !== "agent" || message.traceId !== "trc_running") return message;
|
||||
return { ...message, text: "", status: "running", runnerTrace: trace };
|
||||
});
|
||||
}
|
||||
|
||||
function noFinalRunningTrace(): JsonRecord {
|
||||
const createdAt = new Date().toISOString();
|
||||
return {
|
||||
traceId: "trc_running",
|
||||
status: "running",
|
||||
sessionId: "ses_running",
|
||||
threadId: "thr_running",
|
||||
turnId: "turn_running",
|
||||
events: [{ seq: 1, createdAt, label: "agentrun:backend:turn/running", type: "backend_status", status: "running" }],
|
||||
eventCount: 1,
|
||||
fullTraceLoaded: false,
|
||||
hasMore: false
|
||||
};
|
||||
}
|
||||
|
||||
function completedNoFinalTrace(): JsonRecord {
|
||||
const createdAt = new Date().toISOString();
|
||||
const events = [
|
||||
{ seq: 1, createdAt, label: "agentrun:backend:turn/running", type: "backend_status", status: "running" },
|
||||
{ seq: 2, createdAt, label: "agentrun:backend:turn/completed", type: "backend_status", status: "completed", terminal: true }
|
||||
];
|
||||
return { traceId: "trc_running", status: "completed", sessionId: "ses_running", threadId: "thr_running", turnId: "turn_running", events, eventCount: events.length, fullTraceLoaded: true, hasMore: false };
|
||||
}
|
||||
|
||||
function scrollFollowSession(): SessionRecord {
|
||||
const now = new Date().toISOString();
|
||||
const trace = scrollFollowTrace(90);
|
||||
@@ -865,6 +905,14 @@ function sse(request: IncomingMessage, response: ServerResponse, url: URL): void
|
||||
const scenarioId = state.scenarioId;
|
||||
setTimeout(() => {
|
||||
if (state.scenarioId !== scenarioId) return;
|
||||
if (state.scenarioId === "terminal-completed-no-final-response") {
|
||||
const trace = completedNoFinalTrace();
|
||||
const event = (trace.events as JsonRecord[]).at(-1) ?? { status: "completed", terminal: true };
|
||||
writeSse(response, "workbench.trace.event", { type: "trace.event", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", event, snapshot: trace });
|
||||
finishRunningNoFinalResponse(trace);
|
||||
writeSse(response, "workbench.turn.snapshot", { type: "turn.snapshot", sessionId: "ses_running", threadId: "thr_running", traceId: "trc_running", turn: turnPayload("trc_running") });
|
||||
return;
|
||||
}
|
||||
const terminalStatus = state.terminalFailureScript ? "failed" : "completed";
|
||||
const terminalText = state.terminalFailureScript ? "恢复后失败:缺少受控依赖。" : state.scenarioId === "progress-only-final-response" ? finalOnlyText : "事件重放后完成。";
|
||||
const event = { seq: 3, createdAt: new Date().toISOString(), label: "agentrun:assistant:message", type: "assistant_message", status: terminalStatus, replyAuthority: true, final: true, message: terminalText, terminal: true };
|
||||
@@ -895,6 +943,15 @@ function finishRunningSession(status: "completed" | "failed", text: string): voi
|
||||
session.messages = (session.messages ?? []).map((message) => message.role === "agent" ? { ...message, text, status, runnerTrace: state.traces.trc_running } : message);
|
||||
}
|
||||
|
||||
function finishRunningNoFinalResponse(trace: JsonRecord = completedNoFinalTrace()): void {
|
||||
state.traces.trc_running = trace;
|
||||
const session = sessionById("ses_running");
|
||||
if (!session) return;
|
||||
session.status = "completed";
|
||||
session.updatedAt = new Date().toISOString();
|
||||
session.messages = (session.messages ?? []).map((message) => message.role === "agent" ? { ...message, text: "", status: "completed", runnerTrace: trace } : message);
|
||||
}
|
||||
|
||||
function stateSummary(): JsonRecord {
|
||||
return { scenarioId: state.scenarioId, selectedSessionId: state.selectedSessionId, requestLedger: state.requestLedger, legacyRequestLedger: state.legacyRequestLedger, chatRequests: state.chatRequests, staleTraceId: state.staleTraceId, sessions: state.sessions.map((item) => ({ sessionId: item.sessionId, threadId: item.threadId, status: item.status, lastTraceId: item.lastTraceId, hidden: item.hidden === true })) };
|
||||
}
|
||||
|
||||
@@ -369,7 +369,16 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const replyText = agentReplyText((result as AgentChatResultResponse).reply);
|
||||
const traceAssistantText = assistantTextFromTraceEvents(firstArray((result as AgentChatResultResponse).events, (result as AgentChatResultResponse).traceEvents, runnerTrace?.events));
|
||||
const text = terminal
|
||||
? firstNonEmptyString((result as AgentChatResultResponse).assistantText, finalResponseText((result as AgentChatResultResponse).finalResponse), replyText, errorText, (result as AgentChatResultResponse).text, (result as AgentChatResultResponse).summary, traceAssistantText, message.text, "Code Agent 已完成,但没有返回可展示的 final response。") ?? message.text
|
||||
? terminalAgentMessageText({
|
||||
status,
|
||||
assistantText: (result as AgentChatResultResponse).assistantText,
|
||||
finalText: finalResponseText((result as AgentChatResultResponse).finalResponse),
|
||||
replyText,
|
||||
errorText,
|
||||
resultText: (result as AgentChatResultResponse).text,
|
||||
summaryText: (result as AgentChatResultResponse).summary,
|
||||
traceAssistantText
|
||||
})
|
||||
: message.text;
|
||||
return { ...message, status, text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
|
||||
}));
|
||||
@@ -854,8 +863,17 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
if (!shouldApplyActiveTraceAuthority(traceId, authoritySessionId)) return;
|
||||
const resultTrace = recordValue(result.runnerTrace);
|
||||
const traceAssistantText = assistantTextFromTraceEvents(firstArray(result.events, result.traceEvents, resultTrace?.events));
|
||||
const text = firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), typeof result.reply === "string" ? result.reply : result.reply?.content, agentErrorDisplayText(result.error), result.text, result.summary, traceAssistantText) ?? "Code Agent 已完成,但没有返回可展示的 final response。";
|
||||
const terminalStatus = result.status === "completed" ? "completed" : statusFromResult(result.status);
|
||||
const text = terminalAgentMessageText({
|
||||
status: terminalStatus,
|
||||
assistantText: result.assistantText,
|
||||
finalText: finalResponseText(result.finalResponse),
|
||||
replyText: typeof result.reply === "string" ? result.reply : result.reply?.content,
|
||||
errorText: agentErrorDisplayText(result.error),
|
||||
resultText: result.text,
|
||||
summaryText: result.summary,
|
||||
traceAssistantText
|
||||
});
|
||||
updateActiveMessages((source) => source.map((message) => {
|
||||
if (!shouldApplyTraceToMessage(message, traceId, authoritySessionId)) return message;
|
||||
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
|
||||
@@ -1083,7 +1101,7 @@ function normalizeChatMessage(message: ChatMessage): ChatMessage {
|
||||
const errorText = agentErrorDisplayText(error);
|
||||
const text = role === "agent"
|
||||
? isTerminalMessageStatus(status)
|
||||
? firstNonEmptyString(finalText, errorText, traceAssistantText, baseText) ?? ""
|
||||
? terminalAgentMessageText({ status, finalText, errorText, traceAssistantText, baseText })
|
||||
: ""
|
||||
: firstNonEmptyString(baseText, finalText, traceAssistantText, errorText) ?? "";
|
||||
const messageId = firstNonEmptyString((message as Record<string, unknown>).messageId, message.id) ?? nextProtocolId("msg");
|
||||
@@ -1276,6 +1294,34 @@ function agentReplyText(value: AgentChatResultResponse["reply"]): string | null
|
||||
return value && typeof value === "object" ? firstNonEmptyString(value.content) : null;
|
||||
}
|
||||
|
||||
function terminalAgentMessageText(input: {
|
||||
status: unknown;
|
||||
assistantText?: unknown;
|
||||
finalText?: string | null;
|
||||
replyText?: string | null;
|
||||
errorText?: string | null;
|
||||
resultText?: unknown;
|
||||
summaryText?: unknown;
|
||||
traceAssistantText?: string | null;
|
||||
baseText?: string | null;
|
||||
}): string {
|
||||
const finalText = firstNonEmptyString(
|
||||
input.assistantText,
|
||||
input.finalText,
|
||||
input.replyText,
|
||||
input.resultText,
|
||||
input.summaryText,
|
||||
input.traceAssistantText,
|
||||
input.baseText
|
||||
);
|
||||
if (!isFailureTerminalStatus(input.status)) return finalText ?? "";
|
||||
return firstNonEmptyString(input.errorText, finalText) ?? "";
|
||||
}
|
||||
|
||||
function isFailureTerminalStatus(value: unknown): boolean {
|
||||
return ["failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
|
||||
}
|
||||
|
||||
function messageText(value: unknown): string | null {
|
||||
if (typeof value === "string") return value.trim() || null;
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||||
|
||||
@@ -18,6 +18,31 @@ test("SSE terminal event and REST gap fill replay to the same terminal UI", asyn
|
||||
await expect(page.locator(`${selectors.traceTimeline}[data-status="completed"]`)).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("terminal completed without final response", () => {
|
||||
test.use({ scenarioId: "terminal-completed-no-final-response" });
|
||||
|
||||
test("keeps the assistant final response area empty", async ({ page }, testInfo) => {
|
||||
await gotoWorkbench(page);
|
||||
const agentCard = page.locator(`${selectors.messageCard}[data-role="agent"]`).last();
|
||||
await expect(agentCard).toHaveAttribute("data-status", "running");
|
||||
await expect(agentCard).toHaveAttribute("data-status", "completed");
|
||||
await expect(page.locator(`${selectors.traceTimeline}[data-status="completed"]`)).toBeVisible();
|
||||
|
||||
const snapshot = await agentCard.evaluate((element) => {
|
||||
const placeholder = "Code Agent 已完成,但没有返回可展示的 final response。";
|
||||
return {
|
||||
status: element.getAttribute("data-status"),
|
||||
messageTexts: Array.from(element.querySelectorAll(".message-markdown.message-text")).map((item) => (item.textContent ?? "").trim()),
|
||||
bodyHasPlaceholder: document.body.textContent?.includes(placeholder) === true
|
||||
};
|
||||
});
|
||||
expect(snapshot.status).toBe("completed");
|
||||
expect(snapshot.messageTexts.filter(Boolean)).toEqual([]);
|
||||
expect(snapshot.bodyHasPlaceholder).toBe(false);
|
||||
await saveScreenshot(page, testInfo, "terminal-completed-no-final-response");
|
||||
});
|
||||
});
|
||||
|
||||
test("fresh deep link replays completed turn from the session authority", async ({ page }, testInfo) => {
|
||||
const detail = await page.request.get("/v1/workbench/sessions/ses_completed");
|
||||
expect(detail.status()).toBe(200);
|
||||
|
||||
Reference in New Issue
Block a user