fix: read workbench final response from message parts (#2083)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user