diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index 1591659c..6f2dc5b3 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -1542,6 +1542,7 @@ function agentRunProjectionActivitySignature(result, runnerTrace) { function agentRunProjectionObservedTerminal(result, runnerTrace) { const agentRun = result?.agentRun && typeof result.agentRun === "object" ? result.agentRun : {}; + if (codeAgentPayloadHasSealedFinalResponse(result)) return true; if ( isTraceCommandTerminalStatus(result?.status) || isTraceCommandTerminalStatus(agentRun.status) || @@ -2021,7 +2022,10 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError const lifecycle = codeAgentTurnLifecycleFields(traceId, resultObject ?? snapshotObject ?? {}); const events = Array.isArray(snapshotObject?.events) ? snapshotObject.events : Array.isArray(resultObject?.runnerTrace?.events) ? resultObject.runnerTrace.events : []; const lastEvent = events.at(-1) ?? null; + const finalResponse = resultObject?.finalResponse ?? snapshotObject?.finalResponse ?? snapshotObject?.terminalEvidence?.finalResponse ?? codeAgentFinalResponseEvidence(resultObject ?? snapshotObject ?? {}, traceId); + const sealedFinalStatus = codeAgentPayloadHasSealedFinalResponse(resultObject ?? snapshotObject ?? {}) ? "completed" : null; const status = normalizeTurnStatus( + sealedFinalStatus, resultObject?.status, resultObject?.agentRun?.terminalStatus, resultObject?.agentRun?.commandState, @@ -2056,7 +2060,7 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError runnerTrace: runnerTrace ? compactRunnerTraceForResult(runnerTrace, resultTraceEventLimit(options)) : null, agentRun: resultObject?.agentRun ?? snapshotObject?.agentRun ?? null, terminalEvidence: snapshotObject?.terminalEvidence ?? null, - finalResponse: resultObject?.finalResponse ?? snapshotObject?.finalResponse ?? snapshotObject?.terminalEvidence?.finalResponse ?? null, + finalResponse, traceSummary: resultObject?.traceSummary ?? snapshotObject?.traceSummary ?? snapshotObject?.terminalEvidence?.traceSummary ?? null, retention: snapshotObject?.retention ?? null, eventCount: numberOrNull(snapshotObject?.eventCount ?? runnerTrace?.eventCount ?? events.length), @@ -2726,6 +2730,7 @@ async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options function codeAgentOwnerStatusForResult(result = {}) { const sessionStatus = textValue(result?.session?.status ?? result?.sessionSummary?.status ?? result?.sessionLifecycleStatus); if (sessionStatus === "thread-resume-failed") return "thread-resume-failed"; + if (codeAgentPayloadHasSealedFinalResponse(result)) return "completed"; if (result?.status === "completed") return "completed"; if (result?.status === "canceled" || result?.status === "cancelled") return "canceled"; return result?.status ?? "active"; @@ -2782,7 +2787,7 @@ function codeAgentTraceResultEvidence(payload = {}, params = {}, traceId = null, if (!agentRun && !finalResponse && !traceSummary) return null; return { traceId: resolvedTraceId, - status: payload.status ?? agentRun?.terminalStatus ?? agentRun?.commandState ?? null, + status: codeAgentPayloadHasSealedFinalResponse(payload) ? "completed" : payload.status ?? agentRun?.terminalStatus ?? agentRun?.commandState ?? null, conversationId, sessionId, threadId, @@ -3036,12 +3041,27 @@ function safeMessageId(value) { function codeAgentConversationAgentMessageStatus(payload = {}) { const status = textValue(payload.status ?? payload.agentRun?.terminalStatus ?? payload.commandState ?? payload.runStatus).toLowerCase(); + if (codeAgentPayloadHasSealedFinalResponse(payload)) return "completed"; if (["completed", "done", "success", "active"].includes(status)) return "completed"; if (["failed", "blocked", "error", "timeout", "canceled", "cancelled"].includes(status)) return "failed"; if (payload.error || payload.blocker) return "failed"; return "running"; } +function codeAgentPayloadHasSealedFinalResponse(payload = {}) { + if (!payload || typeof payload !== "object") return false; + if (payload.error || payload.blocker) return false; + if (!codeAgentFinalResponseText(payload)) return false; + const projectionStatus = textValue(payload.projectionStatus ?? payload.projection?.projectionStatus).toLowerCase().replace(/_/gu, "-"); + if (projectionStatus === "caught-up") return true; + const agentRun = payload.agentRun && typeof payload.agentRun === "object" ? payload.agentRun : {}; + return isTraceCommandTerminalStatus(payload.status) || + isTraceCommandTerminalStatus(agentRun.status) || + isTraceCommandTerminalStatus(agentRun.runStatus) || + isTraceCommandTerminalStatus(agentRun.commandState) || + isTraceCommandTerminalStatus(agentRun.terminalStatus); +} + function codeAgentConversationRunnerTraceEvidence(runnerTrace = {}, traceId = null) { const events = Array.isArray(runnerTrace.events) ? runnerTrace.events : []; const lastEvent = runnerTrace.lastEvent && typeof runnerTrace.lastEvent === "object" ? runnerTrace.lastEvent : events.at(-1) ?? null; @@ -3069,22 +3089,26 @@ function codeAgentConversationRunnerTraceEvidence(runnerTrace = {}, traceId = nu } function codeAgentFinalResponseEvidence(payload = {}, traceId = null) { - const text = conversationText(payload.reply?.content ?? payload.message?.content ?? payload.assistantText); + const text = codeAgentFinalResponseText(payload); if (!text) return null; return { text, textChars: text.length, - messageId: payload.reply?.messageId ?? payload.messageId ?? null, + messageId: payload.finalResponse?.messageId ?? payload.reply?.messageId ?? payload.messageId ?? null, role: payload.reply?.role ?? payload.message?.role ?? "assistant", - status: payload.status ?? null, + status: codeAgentPayloadHasSealedFinalResponse(payload) ? "completed" : payload.finalResponse?.status ?? payload.status ?? null, traceId, - createdAt: payload.reply?.createdAt ?? payload.createdAt ?? null, - updatedAt: payload.updatedAt ?? null, - source: "code-agent-result", + createdAt: payload.finalResponse?.createdAt ?? payload.reply?.createdAt ?? payload.createdAt ?? null, + updatedAt: payload.finalResponse?.updatedAt ?? payload.updatedAt ?? null, + source: payload.finalResponse?.source ?? "code-agent-result", valuesPrinted: false }; } +function codeAgentFinalResponseText(payload = {}) { + return conversationText(payload?.finalResponse?.text ?? payload?.finalResponse?.content ?? payload?.reply?.content ?? payload?.message?.content ?? payload?.assistantText); +} + function codeAgentTraceSummaryEvidence(payload = {}, traceId = null, finalResponse = null) { const runnerTrace = payload.runnerTrace && typeof payload.runnerTrace === "object" ? payload.runnerTrace : null; const events = Array.isArray(runnerTrace?.events) ? runnerTrace.events : []; diff --git a/internal/cloud/workbench-projection-writer.ts b/internal/cloud/workbench-projection-writer.ts index 6583d8ff..65a55cb7 100644 --- a/internal/cloud/workbench-projection-writer.ts +++ b/internal/cloud/workbench-projection-writer.ts @@ -244,12 +244,15 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own const projection = createWorkbenchTurnProjection({ traceId: safeId, result: payload, session: { id: resolvedSessionId, status: normalizedStatus, session }, trace: payload?.runnerTrace ?? null }); const timing = projectionTimingForStatus(projection.timing, terminal); const diagnostic = projectionDiagnostics({ traceId: safeId, result: payload, trace: payload?.runnerTrace ?? null, projection }); + const finalText = finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText); const messages = normalizeMessages(session?.messages, { traceId: safeId, sessionId: resolvedSessionId, conversationId: resolvedConversationId, threadId: resolvedThreadId, terminal, + terminalStatus: projection.status ?? normalizedStatus, + finalText, timestamp, timing }); @@ -340,22 +343,28 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own } function normalizeMessages(messages, context) { - if (!Array.isArray(messages)) return []; - return messages.map((message, index) => normalizeMessageFact(message, index, context)).filter(Boolean); + const normalized = Array.isArray(messages) ? messages.map((message, index) => normalizeMessageFact(message, index, context)).filter(Boolean) : []; + if (context?.terminal && context?.finalText && context?.traceId && !normalized.some((message) => message.role !== "user" && message.traceId === context.traceId)) { + const projected = normalizeMessageFact({ role: "agent", status: context.terminalStatus ?? "completed", text: context.finalText, traceId: context.traceId, source: "workbench-terminal-projection" }, normalized.length, context); + if (projected) normalized.push(projected); + } + return normalized; } -function normalizeMessageFact(message = {}, index, { traceId, sessionId, conversationId, threadId, terminal, timestamp, timing: contextTiming }) { +function normalizeMessageFact(message = {}, index, { traceId, sessionId, conversationId, threadId, terminal, terminalStatus = "completed", finalText = null, timestamp, timing: contextTiming }) { const messageId = textValue(message.messageId ?? message.id) || stableFactId("msg", { traceId, index, role: message.role }); if (!sessionId || !messageId) return null; const role = textValue(message.role) || "agent"; - const status = normalizeWorkbenchStatus(message.status ?? (terminal && role !== "user" ? "completed" : role === "user" ? "sent" : "running")); - const messageTerminal = role !== "user" && TERMINAL_STATUSES.has(status); const messageTraceId = safeTraceId(message.traceId ?? message.turnId); - const appliesToContextTrace = role !== "user" && Boolean(traceId && messageTraceId && messageTraceId === traceId); + const appliesToContextTrace = role !== "user" && Boolean(traceId && (!messageTraceId || messageTraceId === traceId)); + const contextTerminalStatus = normalizeWorkbenchStatus(terminalStatus) || "completed"; + const status = appliesToContextTrace && terminal ? contextTerminalStatus : normalizeWorkbenchStatus(message.status ?? (terminal && role !== "user" ? contextTerminalStatus : role === "user" ? "sent" : "running")); + const messageTerminal = role !== "user" && TERMINAL_STATUSES.has(status); const messageTiming = normalizeTimingProjection(message.timing) ?? normalizeTimingProjection(message); const contextTimingProjection = appliesToContextTrace ? normalizeTimingProjection(contextTiming) : null; const timing = role === "user" ? messageTiming ?? emptyTimingProjection() : contextTimingProjection ?? messageTiming ?? emptyTimingProjection(); const terminalCompleted = messageTerminal && status === "completed"; + const text = appliesToContextTrace && terminal && finalText ? finalText : textValue(message.text ?? message.content ?? message.message); return { ...message, messageId, @@ -371,7 +380,7 @@ function normalizeMessageFact(message = {}, index, { traceId, sessionId, convers sourceEventId: textValue(message.sourceEventId) || `${messageId}:0`, terminal: messageTerminal, sealed: messageTerminal, - text: textValue(message.text ?? message.content ?? message.message), + text, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, @@ -384,6 +393,19 @@ function normalizeMessageFact(message = {}, index, { traceId, sessionId, convers }; } +function finalResponseTextValue(...values) { + for (const value of values) { + if (value && typeof value === "object") { + const nested = finalResponseTextValue(value.text, value.content, value.message, value.summary, value.preview); + if (nested) return nested; + continue; + } + const text = textValue(value); + if (text) return text; + } + return null; +} + async function latestWorkbenchCheckpoint(runtimeStore, traceId) { if (typeof runtimeStore?.queryWorkbenchFacts !== "function") return null; const result = await runtimeStore.queryWorkbenchFacts({ traceId, families: ["checkpoints"], limit: 1 });