diff --git a/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts b/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts index 36d4f3d6..3177488d 100644 --- a/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-r1-parity.test.ts @@ -3,7 +3,7 @@ import test from "node:test"; import type { AgentRunProvenance, ChatMessage, TraceEvent } from "../src/types/index.ts"; import { canCancelMessage, canRetryMessage, messageTraceId, renderSafeMarkdown, traceEventBody, traceEventLabel, traceIdentityText, visibleTraceEvents } from "../src/components/workbench/message-rendering.ts"; -import { mergeRunnerTrace } from "../src/composables/useTraceSubscription.ts"; +import { assistantTextFromTraceEvents, mergeRunnerTrace } from "../src/composables/useTraceSubscription.ts"; import { traceDisplayRows, traceNoiseEventCount } from "../../../tools/src/hwlab-cli/trace-renderer.ts"; test("R1 markdown rendering keeps structure and strips unsafe HTML", () => { @@ -103,6 +103,16 @@ test("R1 trace API snapshots stay authoritative over compact result traces", () assert.equal(merged.events?.[1]?.message, "OK"); }); +test("R1 trace event assistant text prefers authoritative final assistant message", () => { + const events: TraceEvent[] = [ + { seq: 1, label: "agentrun:backend:resource-bundle-materialized", message: "bundle ready" }, + { seq: 2, label: "agentrun:assistant:message", type: "assistant", status: "running", replyAuthority: false, final: false, message: "我先检查当前状态。" }, + { seq: 3, label: "agentrun:assistant:message", type: "assistant", status: "completed", replyAuthority: true, final: true, terminal: true, message: "OK" } + ]; + + assert.equal(assistantTextFromTraceEvents(events), "OK"); +}); + test("R1 status summary keeps the restored 23-row floor", () => { const message = agentMessage({ status: "failed", diff --git a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts index d2481586..5ad23c3d 100644 --- a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts +++ b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts @@ -347,7 +347,7 @@ function finalResponseText(value: unknown): string | null { return null; } -function assistantTextFromTraceEvents(events: TraceEvent[]): string | null { +export function assistantTextFromTraceEvents(events: TraceEvent[]): string | null { const terminal = terminalAssistantTextFromTraceEvents(events); if (terminal) return terminal; for (let index = events.length - 1; index >= 0; index -= 1) { diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 1cecb92a..64998441 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -5,7 +5,7 @@ import { computed, ref } from "vue"; import { defineStore } from "pinia"; import { api } from "@/api"; import { connectWorkbenchEvents, type WorkbenchEventStream, type WorkbenchRealtimeEvent } from "@/api/workbench-events"; -import { mergeRunnerTrace, snapshotToRunnerTrace, type TraceSnapshot } from "@/composables/useTraceSubscription"; +import { assistantTextFromTraceEvents, mergeRunnerTrace, snapshotToRunnerTrace, type TraceSnapshot } from "@/composables/useTraceSubscription"; import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiResult, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, TraceEvent, WorkspaceRecord } from "@/types"; import { DEFAULT_WORKBENCH_PROJECT_ID, firstNonEmptyString, nextProtocolId, normalizeWorkbenchConversationId, rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "@/utils"; import { failWorkbenchSessionSwitch, failWorkbenchSubmitJourney, finishWorkbenchSessionSwitchFullLoad, markWorkbenchSubmitApiAccepted, markWorkbenchTraceEventsReceived, markWorkbenchTraceProjected, startWorkbenchSessionSwitch, startWorkbenchSubmitJourney } from "@/utils/workbench-performance"; @@ -459,8 +459,9 @@ export const useWorkbenchStore = defineStore("workbench", () => { const errorText = agentErrorDisplayText(error); const agentRun = agentRunFromResult(result as AgentChatResultResponse, runnerTrace) ?? agentRunFromMessage(message); 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, message.text, "Code Agent 已完成,但没有返回可展示的 final response。") ?? message.text + ? 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 : message.text; return { ...message, status, text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; }); @@ -660,7 +661,8 @@ export const useWorkbenchStore = defineStore("workbench", () => { const terminal = result.terminal === true || isTerminalMessageStatus(status); const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error); const errorText = agentErrorDisplayText(error); - const text = terminal ? firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), agentReplyText(result.reply), errorText, result.text, result.summary, message.text) ?? message.text : message.text; + const traceAssistantText = assistantTextFromTraceEvents(events); + const text = terminal ? firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), agentReplyText(result.reply), errorText, result.text, result.summary, traceAssistantText, message.text) ?? message.text : message.text; const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message); rememberTraceAuthority(runnerTrace); return { ...message, status, text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; @@ -892,7 +894,9 @@ export const useWorkbenchStore = defineStore("workbench", () => { } function completeTrace(traceId: string, result: AgentChatResultResponse): void { - const text = firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), typeof result.reply === "string" ? result.reply : result.reply?.content, agentErrorDisplayText(result.error), result.text, result.summary) ?? "Code Agent 已完成,但没有返回可展示的 final response。"; + 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); messages.value = messages.value.map((message) => { if (message.traceId !== traceId || message.role !== "agent") return message; @@ -1230,10 +1234,11 @@ function normalizeChatMessage(message: ChatMessage): ChatMessage { const status = normalizeChatMessageStatus(message.status); const baseText = firstNonEmptyString(message.text, messageText((message as Record).content), messageText((message as Record).message)); const finalText = firstNonEmptyString(finalResponseText((message as Record).finalResponse), finalResponseText(runnerTrace?.finalResponse)); + const traceAssistantText = assistantTextFromTraceEvents(Array.isArray(runnerTrace?.events) ? runnerTrace.events : []); const errorText = agentErrorDisplayText(error); const text = message.role === "agent" && isTerminalMessageStatus(status) - ? firstNonEmptyString(finalText, errorText, baseText) ?? "" - : firstNonEmptyString(baseText, finalText, errorText) ?? ""; + ? firstNonEmptyString(finalText, errorText, traceAssistantText, baseText) ?? "" + : firstNonEmptyString(baseText, finalText, traceAssistantText, errorText) ?? ""; const messageId = firstNonEmptyString((message as Record).messageId, message.id) ?? nextProtocolId("msg"); return { ...message, text, id: messageId, messageId, title: normalizeWorkbenchMessageTitle(message.role, message.title), createdAt: message.createdAt ?? new Date().toISOString(), status, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined }; }