fix(workbench): preserve terminal final text and timing (#2042)

This commit is contained in:
Lyon
2026-06-24 14:21:21 +08:00
committed by GitHub
parent 3e33eb5803
commit 10e892ce95
+30 -7
View File
@@ -117,7 +117,7 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event
// switching to a different clock origin.
const startedAt = previousTiming?.startedAt ?? explicitStartedAt ?? (sourceSeq <= 1 ? occurredAt : null);
const lastEventAt = latestTimestamp(previousTiming?.lastEventAt, occurredAt);
const finishedAt = terminal ? latestTimestamp(previousTiming?.lastEventAt, optionalTimestampValue(event.finishedAt), occurredAt) : null;
const finishedAt = terminal ? latestTimestamp(previousTiming?.lastEventAt, optionalTimestampValue(event.finishedAt), occurredAt, projectedAt) : null;
const timing = eventTimingProjection({ startedAt, lastEventAt, finishedAt, terminal });
const eventTiming = suppressedAfterSeal ? previousTiming ?? timing : timing;
const sourceEventId = workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt });
@@ -141,7 +141,9 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event
const projectedSeq = nonNegativeInteger(allocation?.projectedSeq);
if (projectedSeq <= 0) throw new Error("Workbench projection allocator returned an invalid projectedSeq.");
const eventId = textValue(event.id) || stableFactId("wte", { traceId, sourceEventId });
const messageFinalText = finalResponseTextValue(event.finalResponse, event.assistantText, event.reply);
const previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText);
const messageFinalText = finalResponseTextValue(event.finalResponse, event.assistantText, event.reply, previousFinalText);
const checkpointFinalResponse = messageFinalText ? { text: messageFinalText, status, traceId, valuesPrinted: false } : null;
const messageFact = sessionId && !suppressedAfterSeal ? normalizeMessageFact({
messageId: eventProjectionAssistantMessageId(traceId, event),
role: "agent",
@@ -249,7 +251,9 @@ function eventProjectionAssistantMessageId(traceId, event = {}) {
async function writeWorkbenchProjectionFacts({ runtimeStore = null, traceStore = defaultCodeAgentTraceStore, traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {} } = {}) {
if (typeof runtimeStore?.writeWorkbenchFacts !== "function") return null;
const facts = buildWorkbenchProjectionFacts({ traceId, ownerUserId, ownerRole, sessionId, projectId, conversationId, threadId, status, session, payload, params });
const safeId = safeTraceId(traceId ?? session?.traceId ?? payload?.traceId ?? params?.traceId);
const previousCheckpoint = safeId ? await latestWorkbenchCheckpoint(runtimeStore, safeId) : null;
const facts = buildWorkbenchProjectionFacts({ traceId, ownerUserId, ownerRole, sessionId, projectId, conversationId, threadId, status, session, payload, params, previousCheckpoint });
if (isEmptyFacts(facts)) return null;
try {
return await runtimeStore.writeWorkbenchFacts({ facts }, {
@@ -273,7 +277,7 @@ async function writeWorkbenchProjectionFacts({ runtimeStore = null, traceStore =
}
}
function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {} } = {}) {
function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {}, previousCheckpoint = null } = {}) {
const safeId = safeTraceId(traceId ?? session?.traceId ?? payload?.traceId ?? params?.traceId);
const resolvedSessionId = textValue(sessionId ?? session?.sessionId ?? payload?.sessionId ?? params?.sessionId) || null;
const resolvedConversationId = textValue(conversationId ?? session?.conversationId ?? payload?.conversationId ?? params?.conversationId) || null;
@@ -285,9 +289,12 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
const projectedStatus = normalizeWorkbenchStatus(projection.status ?? normalizedStatus);
const terminal = projection.terminal === true;
const terminalStatus = terminal ? (TERMINAL_STATUSES.has(projectedStatus) ? projectedStatus : normalizedStatus) : projectedStatus;
const timing = projectionTimingForStatus(projection.timing, terminal);
const projectedAt = new Date().toISOString();
const timing = terminalTimingAtLeastProjectedAt(projectionTimingForStatus(projection.timing, terminal), terminal, projectedAt);
const diagnostic = projectionDiagnostics({ traceId: safeId, result: payload, trace: payload?.runnerTrace ?? null, projection });
const finalText = finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText);
const previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText);
const finalText = finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText, previousFinalText);
const projectedFinalResponse = finalText ? { text: finalText, status: terminalStatus, traceId: safeId, valuesPrinted: false } : projection.finalResponse ?? null;
const messages = normalizeMessages(workbenchProjectionInputMessages({ session, payload, params, traceId: safeId, timestamp, terminal, terminalStatus, finalText }), {
traceId: safeId,
sessionId: resolvedSessionId,
@@ -352,8 +359,10 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
sourceEventId: safeId,
terminal: projection.terminal,
sealed: projection.terminal,
finalResponse: projection.finalResponse,
finalResponse: projectedFinalResponse,
diagnostic,
finalResponse: checkpointFinalResponse,
assistantText: messageFinalText,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
@@ -375,6 +384,8 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own
terminal: projection.terminal,
sealed: projection.terminal,
diagnostic,
finalResponse: projectedFinalResponse,
assistantText: finalText,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
@@ -503,6 +514,18 @@ function projectionTimingForStatus(value, terminal) {
return { ...timing, finishedAt: null, durationMs: null, valuesRedacted: timing.valuesRedacted !== false };
}
function terminalTimingAtLeastProjectedAt(value, terminal, projectedAt) {
const timing = normalizeTimingProjection(value) ?? emptyTimingProjection();
if (!terminal) return timing;
const finishedAt = latestTimestamp(timing.finishedAt, projectedAt);
return {
...timing,
finishedAt,
durationMs: elapsedBetween(timing.startedAt, finishedAt) ?? timing.durationMs,
valuesRedacted: timing.valuesRedacted !== false
};
}
function checkpointIsTerminal(value) {
const source = value && typeof value === "object" ? value : null;
if (!source) return false;