fix: read workbench final response from message parts (#2083)

This commit is contained in:
Lyon
2026-06-25 05:17:58 +08:00
committed by GitHub
parent 19eee3b734
commit fe2ed4e308
3 changed files with 66 additions and 47 deletions
+42 -16
View File
@@ -985,10 +985,11 @@ function factMessageDto(message, parts = [], facts = {}) {
const timing = assistantLike
? factCombinedTimingProjection(status, checkpoint, turn, message)
: factTimingProjection(message, status);
const text = projectionText(message?.text, message?.content, message?.message, message?.finalResponse);
const legacyText = projectionText(message?.text, message?.content, message?.message, message?.finalResponse);
const normalizedParts = parts.length > 0
? [...parts].sort(compareFactPartsAsc).map((part) => factPartDto(part, messageId, traceId)).filter(Boolean)
: text ? [partFact({ type: "text", text, status: message?.status }, 0, messageId, traceId)] : [];
: !assistantLike && legacyText ? [partFact({ type: "text", text: legacyText, status: message?.status }, 0, messageId, traceId)] : [];
const text = factMessageAuthorityText({ role, status, parts: normalizedParts, legacyText });
return {
messageId,
role,
@@ -1012,6 +1013,30 @@ function factMessageDto(message, parts = [], facts = {}) {
};
}
function factMessageAuthorityText({ role, status, parts = [], legacyText = null } = {}) {
if (!isAssistantLikeRole(role)) return firstFactPartText(parts) ?? legacyText ?? "";
if (!isTerminalProjectionStatus(status)) return "";
return firstFactPartText(parts, "final_response") ?? "";
}
function isTerminalProjectionStatus(status) {
return TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status);
}
function firstFactPartText(parts = [], type = null) {
for (const part of parts) {
if (type && part?.type !== type) continue;
const text = projectionText(part?.text);
if (text) return text;
}
return null;
}
function factMessageFinalResponseText(message) {
if (!message || !isAssistantLikeRole(message.role) || !isTerminalProjectionStatus(message.status)) return null;
return firstFactPartText(message.parts, "final_response");
}
function factPartDto(part, messageId, traceId) {
const partId = safePartId(part?.partId) || textValue(part?.partId) || `prt_${hash(`${messageId}:${part?.partIndex ?? 0}:${part?.partType ?? part?.type ?? "text"}`).slice(0, 24)}`;
const text = projectionText(part?.text, part?.content, part?.message);
@@ -1049,20 +1074,21 @@ function factTurnSnapshot({ turn = null, session = null, facts = {}, traceId, tu
const checkpointStatus = normalizeTerminalStatus(checkpoint?.status);
const traceStatus = normalizeTerminalStatus(trace.status);
const status = normalizeStatus(checkpointStatus ?? traceStatus ?? turn?.status ?? session?.status);
const assistantText = projectionText(turn?.finalResponse, turn?.assistantText, turn?.text, assistantMessage?.text);
const terminal = isTerminalProjectionStatus(status);
const assistantText = terminal ? factMessageFinalResponseText(assistantMessage) : null;
const timing = factCombinedTimingProjection(status, checkpoint, turn, trace);
return {
turnId: resolvedTurnId,
traceId: safeTrace,
status,
running: RUNNING_STATUSES.has(status),
terminal: TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status),
terminal,
sessionId: factSessionId(session) ?? turn?.sessionId ?? checkpoint?.sessionId ?? null,
threadId: safeOpaqueId(session?.threadId) ?? (textValue(session?.threadId) || null),
userMessageId: userMessage?.messageId ?? null,
assistantMessageId: assistantMessage?.messageId ?? turn?.messageId ?? null,
assistantText: assistantText ?? null,
finalResponse: turn?.finalResponse ?? null,
finalResponse: assistantText ? { text: assistantText, sealed: true, source: "message-part" } : null,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
@@ -2006,7 +2032,7 @@ function sessionMessages(session, options = {}) {
.map((message) => applyTerminalMessageProjection(message, terminalProjection))
.map((message) => applyProjectionMessageDiagnostic(message, options.projectionDiagnostic, currentTurn));
const hasAssistantLikeFinal = messages.some((message) => isAssistantLikeRole(message.role) && (!finalTraceId || message.traceId === finalTraceId));
const finalText = terminalProjection?.text ?? projectionText(snapshot.finalResponse);
const finalText = terminalProjection?.text ?? null;
if (!hasAssistantLikeFinal && finalText) {
const finalMessageStatus = terminalProjection ? terminalProjection.status : sessionLifecycleProjectionStatus(session);
const finalMessageSource = terminalProjection ? terminalProjection.source : "finalResponse";
@@ -2033,18 +2059,16 @@ function applyProjectionMessageDiagnostic(message, diagnostic, currentTurn) {
function terminalMessageProjection(snapshot, options, currentTurn) {
if (!currentTurn) return null;
const traceId = currentTurn.traceId;
const result = options.result ?? options.codeAgentChatResults?.get?.(traceId) ?? null;
const trace = currentTurn.trace;
const status = currentTurn.status;
if (!TERMINAL_STATUSES.has(status) || RUNNING_STATUSES.has(status)) return null;
const text = projectionText(currentTurn.finalResponse, result?.finalResponse, result?.assistantText, result?.reply, result?.text, result?.summary, trace?.finalResponse, trace?.terminalEvidence?.finalResponse, snapshot.finalResponse);
const text = projectionText(currentTurn.finalResponse);
return { traceId, status, text, source: "turn-projection" };
}
function applyTerminalMessageProjection(message, projection) {
if (!projection || !isAssistantLikeRole(message.role)) return message;
if (projection.traceId && message.traceId && message.traceId !== projection.traceId) return message;
const text = projection.text || message.text || "";
const text = projection.text || "";
const parts = projectTerminalMessageParts(message.parts, message.messageId, projection.traceId ?? message.traceId, projection, text);
return {
...message,
@@ -2060,12 +2084,12 @@ function applyTerminalMessageProjection(message, projection) {
function projectTerminalMessageParts(parts, messageId, traceId, projection, text) {
const sourceParts = Array.isArray(parts) ? parts : [];
const textIndex = sourceParts.findIndex((part) => part?.type === "text" || part?.text);
const textIndex = sourceParts.findIndex((part) => part?.type === "final_response" || part?.partType === "final_response");
if (textIndex >= 0) {
return sourceParts.map((part, index) => index === textIndex ? { ...part, traceId, text: text || part.text || null, status: projection.status } : part);
return sourceParts.map((part, index) => index === textIndex ? { ...part, traceId, type: "final_response", text: text || part.text || null, status: projection.status } : part);
}
if (!text) return sourceParts;
return [partFact({ type: "text", text, status: projection.status }, 0, messageId, traceId), ...sourceParts];
return [partFact({ type: "final_response", text, status: projection.status }, 0, messageId, traceId), ...sourceParts];
}
function projectionText(...values) {
@@ -2085,18 +2109,20 @@ function messageFact(message, index, session, snapshot) {
const role = textValue(message?.role) || (index % 2 === 0 ? "user" : "assistant");
const traceId = safeTraceId(message?.traceId ?? message?.turnTraceId ?? session.lastTraceId ?? snapshot.lastTraceId) ?? null;
const messageId = safeMessageId(message?.messageId ?? message?.id) || `msg_${hash(`${session.id}:${index}:${role}:${traceId ?? "none"}`).slice(0, 24)}`;
const text = projectionText(message?.text, message?.content, message?.message, message?.finalResponse);
const status = normalizeStatus(message?.status ?? (isAssistantLikeRole(role) ? session.status : "completed"));
const legacyText = projectionText(message?.text, message?.content, message?.message, message?.finalResponse);
const rawParts = Array.isArray(message?.parts) ? message.parts : [];
const parts = rawParts.length > 0
? rawParts.map((part, partIndex) => partFact(part, partIndex, messageId, traceId)).filter(Boolean)
: text ? [partFact({ type: "text", text }, 0, messageId, traceId)] : [];
: !isAssistantLikeRole(role) && legacyText ? [partFact({ type: "text", text: legacyText }, 0, messageId, traceId)] : [];
const text = factMessageAuthorityText({ role, status, parts, legacyText });
return {
messageId,
role,
sessionId: session.id,
traceId,
turnId: safeTurnId(message?.turnId) || traceId,
status: normalizeStatus(message?.status ?? (isAssistantLikeRole(role) ? session.status : "completed")),
status,
parts,
text: text || "",
textPreview: text ? text.slice(0, 240) : null,