From 270cc999d47d18b9e7943f7158ea0f11afa2a0c7 Mon Sep 17 00:00:00 2001 From: lyon Date: Fri, 19 Jun 2026 04:56:47 +0800 Subject: [PATCH] fix: remove workbench read-side final text inference --- .../scripts/workbench-r1-parity.test.ts | 12 +-- web/hwlab-cloud-web/src/api/workbench.ts | 32 +++---- .../src/composables/useTraceSubscription.ts | 51 +----------- .../src/stores/workbench-session.ts | 6 +- web/hwlab-cloud-web/src/stores/workbench.ts | 83 +++++-------------- 5 files changed, 40 insertions(+), 144 deletions(-) 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 1591a7cc..36d4f3d6 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 { assistantTextFromTraceEvents, mergeRunnerTrace } from "../src/composables/useTraceSubscription.ts"; +import { 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,16 +103,6 @@ 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", 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/api/workbench.ts b/web/hwlab-cloud-web/src/api/workbench.ts index b80c0fcd..120b3f7f 100644 --- a/web/hwlab-cloud-web/src/api/workbench.ts +++ b/web/hwlab-cloud-web/src/api/workbench.ts @@ -91,9 +91,7 @@ function normalizeWorkbenchTurnResult(result: ApiResult> if (!result.ok || !result.data) return result as ApiResult; const turn = recordValue(result.data.turn) ?? result.data; const trace = recordValue(turn.trace); - const envelopeStatus = textValue(result.data.status); - const status = normalizeStatus(textValue(turn.status) ?? (envelopeStatus && !["ok", "succeeded", "found"].includes(envelopeStatus) ? envelopeStatus : null)) ?? "unknown"; - const terminal = turn.terminal === true || isTerminalStatus(status); + const status = normalizeStatus(textValue(turn.status)) ?? "unknown"; return { ...result, data: { @@ -103,8 +101,8 @@ function normalizeWorkbenchTurnResult(result: ApiResult> lastProjectedSeq: Number.isFinite(Number(result.data.lastProjectedSeq)) ? Number(result.data.lastProjectedSeq) : undefined, traceId: textValue(turn.traceId) ?? traceId, status, - running: !terminal && (turn.running === true || isActiveStatus(status)), - terminal, + running: typeof turn.running === "boolean" ? turn.running : undefined, + terminal: typeof turn.terminal === "boolean" ? turn.terminal : undefined, sessionId: textValue(turn.sessionId) ?? undefined, threadId: textValue(turn.threadId) ?? undefined, agentRun: recordValue(turn.agentRun) ?? undefined, @@ -117,30 +115,32 @@ function normalizeWorkbenchTraceResult(result: ApiResult if (!result.ok || !result.data) return result as ApiResult; const events = Array.isArray(result.data.events) ? result.data.events : []; const nextSeq = Number(result.data.nextSeq ?? result.data.nextSinceSeq); - const traceStatus = textValue(result.data.traceStatus, result.data.status) ?? "unknown"; + const traceStatus = textValue(result.data.traceStatus) ?? undefined; const eventCount = Number.isFinite(Number(result.data.eventCount)) ? Number(result.data.eventCount) : events.length; const normalizedNextSeq = Number.isFinite(nextSeq) ? Math.trunc(nextSeq) : null; + const hasMore = result.data.hasMore === true; + const fullTraceLoaded = typeof result.data.fullTraceLoaded === "boolean" ? result.data.fullTraceLoaded : undefined; return { ...result, data: { ...result.data, traceId: textValue(result.data.traceId) ?? traceId, - status: traceStatus, + status: undefined, traceStatus, events, traceEvents: events, eventCount, - hasMore: result.data.hasMore === true, - fullTraceLoaded: result.data.hasMore !== true, + hasMore, + fullTraceLoaded, nextSinceSeq: normalizedNextSeq, range: recordValue(result.data.range) ?? undefined, runnerTrace: { traceId: textValue(result.data.traceId) ?? traceId, - status: traceStatus, + status: undefined, events, eventCount, - hasMore: result.data.hasMore === true, - fullTraceLoaded: result.data.hasMore !== true, + hasMore, + fullTraceLoaded, nextSinceSeq: normalizedNextSeq, range: recordValue(result.data.range) ?? undefined, eventSource: "trace-api" @@ -156,14 +156,6 @@ function normalizeStatus(value: unknown): string | null { return normalized === "cancelled" ? "canceled" : normalized; } -function isActiveStatus(value: unknown): boolean { - return ["accepted", "active", "busy", "creating", "pending", "processing", "running"].includes(normalizeStatus(value) ?? ""); -} - -function isTerminalStatus(value: unknown): boolean { - return ["blocked", "canceled", "completed", "failed", "stale", "thread-resume-failed", "timeout"].includes(normalizeStatus(value) ?? ""); -} - function recordValue(value: unknown): Record | null { return value && typeof value === "object" ? value as Record : null; } diff --git a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts index 47846e24..e6928f35 100644 --- a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts +++ b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts @@ -144,12 +144,7 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac error: trace.error ?? terminal.error }; const events = Array.isArray(trace.events) ? trace.events : []; - const assistantText = bestAssistantTextCandidate([ - finalResponseText(mergedTrace.finalResponse), - terminalAssistantTextFromTraceEvents(events), - firstNonEmptyResultText(terminal), - assistantTextFromTraceEvents(events) - ]); + const assistantText = firstNonEmptyString(finalResponseText(mergedTrace.finalResponse), terminal.assistantText); return { ...terminal, traceId: mergedTrace.traceId ?? terminal.traceId, @@ -333,12 +328,6 @@ function sleep(ms: number): Promise { return new Promise((resolve) => window.setTimeout(resolve, ms)); } -function firstNonEmptyResultText(result: AgentChatResultResponse): string | null { - const reply = result.reply; - const replyText = typeof reply === "string" ? reply : typeof reply?.content === "string" ? reply.content : null; - return firstNonEmptyString(result.assistantText, replyText); -} - function finalResponseText(value: unknown): string | null { if (typeof value === "string") return value; if (value && typeof value === "object") { @@ -347,41 +336,3 @@ function finalResponseText(value: unknown): string | null { } return 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) { - const event = events[index]; - if (!event || !isAssistantTraceEvent(event)) continue; - const text = firstNonEmptyString(event.message, event.text, event.outputSummary); - if (text) return text; - } - return null; -} - -function terminalAssistantTextFromTraceEvents(events: TraceEvent[]): string | null { - for (let index = events.length - 1; index >= 0; index -= 1) { - const event = events[index]; - if (!event || !isTerminalAssistantTraceEvent(event)) continue; - const text = firstNonEmptyString(event.message, event.text, event.outputSummary); - if (text) return text; - } - return null; -} - -function bestAssistantTextCandidate(candidates: Array): string | null { - return candidates.find((candidate) => typeof candidate === "string" && candidate.trim())?.trim() ?? null; -} - -function isAssistantTraceEvent(event: TraceEvent): boolean { - const label = String(event.label ?? ""); - return label === "agentrun:assistant:message" || label === "assistant:message" || label === "assistant:completed" || event.type === "assistant" || event.type === "assistant_message"; -} - -function isTerminalAssistantTraceEvent(event: TraceEvent): boolean { - const label = String(event.label ?? ""); - if (label === "assistant:completed") return true; - if (label === "agentrun:assistant:message") return event.replyAuthority === true || event.final === true || event.terminal === true || String(event.status ?? "") === "completed"; - return event.type === "assistant_message" && (event.status === "completed" || event.final === true || event.terminal === true); -} diff --git a/web/hwlab-cloud-web/src/stores/workbench-session.ts b/web/hwlab-cloud-web/src/stores/workbench-session.ts index ffce3d7c..6a584e9e 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-session.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-session.ts @@ -78,8 +78,9 @@ export function resolveComposerState(input: { messages: ChatMessage[]; sessions? const turn = activeTraceId ? input.turnStatusAuthority?.[activeTraceId] : null; const activeByRequest = Boolean(currentRequest && isActiveStatus(currentRequest.status ?? "running")); const activeByMessage = latestMessage?.role === "agent" && isActiveStatus(latestMessage.status); - const activeByStatus = turn?.running === true || isActiveStatus(turn?.status) || activeByRequest || activeByMessage; - const terminal = turn?.terminal === true || isTerminalStatus(turn?.status); + const terminalByMessage = latestMessage?.role === "agent" && isTerminalStatus(latestMessage.status); + const activeByStatus = !terminalByMessage && (turn?.running === true || isActiveStatus(turn?.status) || activeByRequest || activeByMessage); + const terminal = terminalByMessage || turn?.terminal === true || isTerminalStatus(turn?.status); const active = activeSession(input.sessions ?? [], sessionId); const effectiveSessionId = firstNonEmptyString(turn?.sessionId, currentRequest?.sessionId, active?.sessionId, sessionId); const threadId = firstNonEmptyString(turn?.threadId, currentRequest?.threadId, active?.threadId); @@ -98,6 +99,7 @@ export function resolveCancelableAgentMessage(input: { messages: ChatMessage[]; if (message.role !== "agent") continue; if (firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) !== targetTraceId) continue; if (!messageBelongsToCancelTarget(message, input)) continue; + if (isTerminalStatus(message.status)) continue; return message; } return null; diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index fdc880b8..b06b592e 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, nextTick, ref } from "vue"; import { defineStore } from "pinia"; import { api } from "@/api"; import { connectWorkbenchEvents, type WorkbenchEventStream, type WorkbenchRealtimeEvent } from "@/api/workbench-events"; -import { assistantTextFromTraceEvents, mergeRunnerTrace, snapshotToRunnerTrace, type TraceSnapshot } from "@/composables/useTraceSubscription"; +import { mergeRunnerTrace, snapshotToRunnerTrace, type TraceSnapshot } from "@/composables/useTraceSubscription"; import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiResult, ChatMessage, LiveSurface, ProviderProfile, TraceEvent, WorkbenchSessionRecord } from "@/types"; import { firstNonEmptyString, nextProtocolId, normalizeWorkbenchSessionId, normalizeWorkbenchSessionRouteId } from "@/utils"; import { failWorkbenchSessionSwitch, failWorkbenchSubmitJourney, finishWorkbenchSessionSwitchFullLoad, markWorkbenchSubmitApiAccepted, markWorkbenchTraceEventsReceived, markWorkbenchTraceProjected, startWorkbenchSessionSwitch, startWorkbenchSubmitJourney } from "@/utils/workbench-performance"; @@ -356,28 +356,23 @@ export const useWorkbenchStore = defineStore("workbench", () => { } function syncTurnStatusToMessage(traceId: string, result: AgentChatResultResponse | TraceSnapshot): void { - const status = statusFromResult(result.status); - const terminal = (result as AgentChatResultResponse).terminal === true || isTerminalMessageStatus(status); + const resultStatus = statusFromResult(result.status); const authoritySessionId = traceResultSessionId(result); updateActiveMessages((source) => source.map((message) => { if (!shouldApplyTraceToMessage(message, traceId, authoritySessionId)) return message; + const status = isTerminalMessageStatus(message.status) ? message.status : resultStatus; + const terminal = isTerminalMessageStatus(status) || (result as AgentChatResultResponse).terminal === true; const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result as AgentChatResultResponse); rememberTraceAuthority(runnerTrace); const error = normalizeAgentError((result as AgentChatResultResponse).error ?? runnerTrace?.error ?? message.error); 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 - ? terminalAgentMessageText({ + ? projectedAgentMessageText({ 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 + baseText: message.text }) : message.text; return { ...message, status, text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; @@ -563,12 +558,12 @@ export const useWorkbenchStore = defineStore("workbench", () => { updatedAt: new Date().toISOString() }); const explicitStatus = normalizedStatusText(result.status); - const status = explicitStatus ? statusFromResult(result.status) : message.status; + const resultStatus = explicitStatus ? statusFromResult(result.status) : message.status; + const status = isTerminalMessageStatus(message.status) ? message.status : resultStatus; const terminal = result.terminal === true || isTerminalMessageStatus(status); const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error); const errorText = agentErrorDisplayText(error); - 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 text = terminal ? projectedAgentMessageText({ status, finalText: finalResponseText(result.finalResponse), errorText, baseText: 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() }; @@ -848,15 +843,14 @@ export const useWorkbenchStore = defineStore("workbench", () => { if (!shouldApplyTraceToMessage(message, traceId, authoritySessionId)) return message; const runnerTrace = mergeRunnerTrace(message.runnerTrace, trace); rememberTraceAuthority(runnerTrace); - const status = statusFromResult(firstNonEmptyString(turn?.status, message.status) ?? message.status); - const terminal = turn?.terminal === true || isTerminalMessageStatus(turn?.status); - const traceAssistantText = message.role === "agent" ? assistantTextFromTraceEvents(Array.isArray(runnerTrace.events) ? runnerTrace.events : []) : null; + const status = message.status; + const terminal = isTerminalMessageStatus(status); const error = message.role === "agent" ? normalizeAgentError(runnerTrace.error ?? message.error) : normalizeAgentError(message.error); const errorText = message.role === "agent" ? agentErrorDisplayText(error) : null; const nextText = message.role === "agent" && terminal - ? firstNonEmptyString(finalResponseText(runnerTrace.finalResponse), errorText, traceAssistantText, message.text) ?? message.text + ? projectedAgentMessageText({ status, finalText: finalResponseText(runnerTrace.finalResponse), errorText, baseText: message.text }) : message.text; - return { ...message, status, text: nextText, traceAutoLifecycle: terminal ? "terminal" : "running", runnerTrace, error: error ?? message.error ?? null, updatedAt: new Date().toISOString() }; + return { ...message, status, text: nextText, traceAutoLifecycle: terminal ? "terminal" : message.traceAutoLifecycle, runnerTrace, error: error ?? message.error ?? null, updatedAt: new Date().toISOString() }; })); markWorkbenchTraceProjected(traceId); void refreshSessions(trace.sessionId ?? selectedSessionId.value); @@ -865,25 +859,16 @@ export const useWorkbenchStore = defineStore("workbench", () => { function completeTrace(traceId: string, result: AgentChatResultResponse): void { const authoritySessionId = traceResultSessionId(result); if (!shouldApplyActiveTraceAuthority(traceId, authoritySessionId)) return; - const resultTrace = recordValue(result.runnerTrace); - const traceAssistantText = assistantTextFromTraceEvents(firstArray(result.events, result.traceEvents, resultTrace?.events)); 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 - }); + const finalText = finalResponseText(result.finalResponse); + const errorText = agentErrorDisplayText(result.error); updateActiveMessages((source) => source.map((message) => { if (!shouldApplyTraceToMessage(message, traceId, authoritySessionId)) return message; const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result); rememberTraceAuthority(runnerTrace); const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error); const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message); + const text = projectedAgentMessageText({ status: terminalStatus, finalText, errorText, baseText: message.text }); return { ...message, status: terminalStatus, text, traceAutoLifecycle: "terminal", runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; })); rememberTurnStatus(traceId, result); @@ -1101,13 +1086,12 @@ 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 = role === "agent" ? isTerminalMessageStatus(status) - ? terminalAgentMessageText({ status, finalText, errorText, traceAssistantText, baseText }) + ? projectedAgentMessageText({ status, finalText, errorText, baseText }) : "" - : firstNonEmptyString(baseText, finalText, traceAssistantText, errorText) ?? ""; + : firstNonEmptyString(baseText, finalText, errorText) ?? ""; const messageId = firstNonEmptyString((message as Record).messageId, message.id) ?? nextProtocolId("msg"); return { ...message, role, text, id: messageId, messageId, title: normalizeWorkbenchMessageTitle(role, message.title), createdAt: message.createdAt ?? new Date().toISOString(), status, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined }; } @@ -1306,33 +1290,10 @@ function finalResponseText(value: unknown): string | null { return firstNonEmptyString(messageText(record.text), messageText(record.content), messageText(record.message)); } -function agentReplyText(value: AgentChatResultResponse["reply"]): string | null { - if (typeof value === "string") return value; - 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 projectedAgentMessageText(input: { status: unknown; finalText?: string | null; errorText?: string | null; baseText?: string | null }): string { + const text = firstNonEmptyString(input.finalText, input.baseText); + if (!isFailureTerminalStatus(input.status)) return text ?? ""; + return firstNonEmptyString(input.errorText, text) ?? ""; } function isFailureTerminalStatus(value: unknown): boolean {