/* * SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p0-projector-resume; draft-2026-06-20-p1-zero-split-durable-realtime; PJ2026-010403 API契约 draft-2026-06-18-r1. * 职责: 生成唯一 Workbench turn projection;trace event 状态只作为输入证据,不直接成为 turn lifecycle。 */ export const TERMINAL_STATUSES = new Set(["completed", "failed", "blocked", "timeout", "cancelled", "canceled", "idle"]); export const RUNNING_STATUSES = new Set(["running", "retrying", "pending", "queued", "accepted", "dispatching", "streaming", "active", "processing", "busy", "creating"]); export function createWorkbenchTurnProjection({ turnId = null, traceId = null, result = null, session = null, trace = null } = {}) { const projectionTraceId = textValue(traceId ?? trace?.traceId ?? result?.traceId ?? session?.lastTraceId) || null; const projectionTurnId = textValue(turnId) || projectionTraceId; const traceTerminal = traceTerminalEvidence(trace); const terminalEvidence = terminalTurnEvidence({ result, traceTerminal }); const activeEvidence = activeTurnEvidence({ result, session, trace }); const status = terminalEvidence?.status ?? activeEvidence?.status ?? "unknown"; const running = RUNNING_STATUSES.has(status); const terminal = Boolean(terminalEvidence && TERMINAL_STATUSES.has(status) && !running); const finalText = terminal ? projectionText(result?.finalResponse, result?.assistantText, result?.reply, result?.text, result?.summary, trace?.finalResponse, trace?.terminalEvidence?.finalResponse, terminalEvidence?.finalResponse) : null; const agentRun = objectValue(result?.agentRun ?? trace?.agentRun); const lastEvent = traceLastEvent(trace); const timing = createWorkbenchTurnTimingProjection({ result, session, trace, status, terminal }); return { turnId: projectionTurnId, traceId: projectionTraceId, status, running, terminal, source: terminalEvidence?.source ?? activeEvidence?.source ?? null, terminalEvidence, finalResponse: finalText ? { text: finalText, status, traceId: projectionTraceId, valuesPrinted: false } : null, assistantText: finalText, lastProjectedSeq: traceLastSeq(trace), sourceRunId: agentRun?.runId ?? lastEvent?.runId ?? lastEvent?.payload?.runId ?? null, sourceCommandId: agentRun?.commandId ?? lastEvent?.commandId ?? lastEvent?.payload?.commandId ?? null, eventCount: normalizedEventCount(trace), updatedAt: trace?.updatedAt ?? result?.updatedAt ?? session?.updatedAt ?? null, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, finishedAt: timing.finishedAt, durationMs: timing.durationMs, valuesRedacted: true }; } export function createWorkbenchTurnTimingProjection({ result = null, session = null, trace = null, status = null, terminal = null } = {}) { const events = Array.isArray(trace?.events) ? trace.events : []; const firstEvent = events[0] ?? null; const lastEvent = traceLastEvent(trace); const terminalEvent = terminalTraceEventForTiming(events); // Session lifecycle timing is not turn timing authority. Mixing session // startedAt/createdAt with turn terminal duration makes the running timer // jump backwards when the turn completes. const directTiming = objectValue(result?.timing) ?? objectValue(trace?.timing) ?? null; const agentRun = objectValue(result?.agentRun ?? trace?.agentRun); const traceSummary = objectValue(result?.traceSummary ?? trace?.traceSummary); const sessionSnapshot = objectValue(session?.session); const normalizedStatus = normalizeWorkbenchStatus(status ?? result?.status ?? trace?.status ?? session?.status); const isTerminal = typeof terminal === "boolean" ? terminal : TERMINAL_STATUSES.has(normalizedStatus) && !RUNNING_STATUSES.has(normalizedStatus); const startedAt = firstTimestamp( result?.startedAt, directTiming?.startedAt, result?.traceStartedAt, trace?.startedAt, traceSummary?.startedAt, firstEvent?.startedAt, firstEvent?.createdAt, firstEvent?.occurredAt ); const lastEventAt = firstTimestamp( result?.lastEventAt, directTiming?.lastEventAt, trace?.lastEventAt, traceSummary?.lastEventAt, lastEvent?.lastEventAt, lastEvent?.updatedAt, lastEvent?.createdAt, lastEvent?.occurredAt, trace?.updatedAt, result?.updatedAt ); const finishedAt = isTerminal ? firstTimestamp( result?.finishedAt, directTiming?.finishedAt, trace?.finishedAt, traceSummary?.finishedAt, trace?.terminalEvidence?.updatedAt, terminalEvent?.updatedAt, terminalEvent?.createdAt, terminalEvent?.occurredAt, result?.updatedAt, trace?.updatedAt, lastEventAt ) : null; const durationMs = durationValue( result?.durationMs, directTiming?.durationMs, trace?.durationMs, traceSummary?.durationMs, trace?.elapsedMs, result?.elapsedMs, elapsedBetween(startedAt, finishedAt ?? lastEventAt) ); return { startedAt, lastEventAt, finishedAt, durationMs, valuesRedacted: true }; } export function projectionDiagnostics({ traceId = null, projection = null, result = null, trace = null, refreshError = null } = {}) { const turn = projection ?? createWorkbenchTurnProjection({ traceId, result, trace }); const sealedCompleted = turn.terminal === true && turn.status === "completed" && Boolean(turn.finalResponse?.text); const source = sealedCompleted ? null : projectionDiagnosticSource(trace); const rawBlocker = sealedCompleted ? null : refreshError ?? source?.blocker ?? trace?.blocker ?? result?.blocker ?? result?.error ?? null; const retryingProviderInterruption = !sealedCompleted && turn.running === true ? retryableProviderInterruptionEvidence(rawBlocker) : null; const blocker = retryingProviderInterruption ? null : rawBlocker; const hasProjectionInput = hasTraceProjection(trace) || Boolean(result || result?.agentRun); const sourceStatus = sealedCompleted ? null : normalizeProjectionStatus(source?.projectionStatus ?? trace?.projectionStatus); const effectiveSourceStatus = retryingProviderInterruption && sourceStatus === "blocked" ? "projecting" : sourceStatus; const status = sealedCompleted ? "caught-up" : effectiveSourceStatus ?? (blocker ? "blocked" : turn.terminal ? "caught-up" : hasProjectionInput ? "projecting" : "unknown"); const diagnostic = blocker ? diagnosticBlocker(blocker) : null; const sourceHealth = sealedCompleted ? null : normalizeProjectionHealth(source?.projectionHealth ?? trace?.projectionHealth); const effectiveSourceHealth = retryingProviderInterruption && (sourceHealth === "degraded" || sourceHealth === "unavailable" || sourceHealth === "stalled") ? "projecting" : sourceHealth; const projectionHealth = sealedCompleted ? "healthy" : effectiveSourceHealth ?? projectionHealthFor({ status, turn, hasProjectionInput, blocker: diagnostic }); const staleMs = sealedCompleted ? null : projectionStaleMs(source?.staleMs ?? trace?.staleMs, turn.updatedAt ?? trace?.updatedAt ?? result?.updatedAt); return { projectionStatus: status, projectionHealth, lastProjectedSeq: turn.lastProjectedSeq ?? null, sourceRunId: turn.sourceRunId ?? null, sourceCommandId: turn.sourceCommandId ?? null, staleMs, blocker: diagnostic, retryingProviderInterruption, updatedAt: turn.updatedAt ?? null, valuesRedacted: true }; } export function durableTraceStatus(events = []) { const terminal = terminalTraceEventEvidence(events); if (terminal) return terminal.status; const activeStatus = activeTraceEventStatus(events); return activeStatus ?? (events.length > 0 ? "running" : "missing"); } export function traceTerminalEvidence(trace = null) { const direct = objectValue(trace?.terminalEvidence); if (direct) { const status = terminalStatusFromValue(direct.status ?? direct.terminalStatus ?? trace?.status) ?? "completed"; if (status !== "completed" && retryableProviderInterruptionEvidence(direct, trace)) return null; return { source: "trace-terminal-evidence", status, evidence: direct, valuesRedacted: true }; } const finalResponse = objectValue(trace?.finalResponse); if (finalResponse) { const status = terminalStatusFromValue(finalResponse.status ?? trace?.status) ?? "completed"; return { source: "trace-final-response", status, evidence: { textPresent: Boolean(projectionText(finalResponse)), valuesRedacted: true }, valuesRedacted: true }; } const events = Array.isArray(trace?.events) ? trace.events : []; return terminalTraceEventEvidence(events); } export function normalizeWorkbenchStatus(value) { const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); if (text === "cancelled") return "canceled"; return text || "unknown"; } function terminalTurnEvidence({ result = null, traceTerminal = null } = {}) { const resultStatus = terminalStatusFromValue( result?.terminalStatus ?? result?.agentRun?.terminalStatus ?? result?.agentRun?.commandState ?? result?.agentRun?.status ?? result?.agentRun?.runStatus ); if (resultStatus) { if (resultStatus !== "completed" && retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, traceTerminal?.evidence)) return null; return { source: "result", status: resultStatus, finalResponse: traceTerminal?.finalResponse ?? null, valuesRedacted: true }; } const statusOnly = terminalStatusFromValue(result?.status); if (statusOnly && resultHasTerminalAuthority(result, traceTerminal)) { if (statusOnly !== "completed" && retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, traceTerminal?.evidence)) return null; return { source: "result", status: statusOnly, finalResponse: traceTerminal?.finalResponse ?? null, valuesRedacted: true }; } if (traceTerminal && retryableProviderInterruptionEvidence(traceTerminal.evidence)) return null; return traceTerminal; } function resultHasTerminalAuthority(result = null, traceTerminal = null) { if (!result || typeof result !== "object") return false; if (traceTerminal) return true; if (result.terminal === true || result.sealed === true) return true; if (result.error || result.blocker) return true; if (firstTimestamp(result.finishedAt, result.completedAt, result.endedAt)) return true; const finalText = projectionText(result.finalResponse, result.assistantText, result.reply, result.text, result.summary); return Boolean(finalText && terminalStatusFromValue(result.status)); } function activeTurnEvidence({ result = null, session = null, trace = null } = {}) { const retryEvidence = retryableProviderInterruptionEvidence(result, result?.agentRun, result?.providerTrace, trace); if (retryEvidence) return { source: "provider-retry", status: "retrying", evidence: retryEvidence, valuesRedacted: true }; const resultStatus = firstStatus([ result?.status, result?.agentRun?.status, result?.agentRun?.commandState, result?.agentRun?.runStatus ], RUNNING_STATUSES); if (resultStatus) return { source: "result", status: normalizeActiveStatus(resultStatus), valuesRedacted: true }; const traceStatus = firstStatus([trace?.status, trace?.traceStatus], RUNNING_STATUSES); if (traceStatus) return { source: "trace", status: normalizeActiveStatus(traceStatus), valuesRedacted: true }; if (hasTraceProjection(trace)) return { source: "trace-events", status: "running", valuesRedacted: true }; const sessionStatus = firstStatus([session?.status, session?.session?.sessionStatus], RUNNING_STATUSES); if (sessionStatus) return { source: "session", status: normalizeActiveStatus(sessionStatus), valuesRedacted: true }; return null; } function firstStatus(values, accepted) { for (const value of values) { const status = normalizeWorkbenchStatus(value); if (accepted.has(status)) return status; } return null; } function terminalStatusFromValue(value) { const status = normalizeWorkbenchStatus(value); return TERMINAL_STATUSES.has(status) && !RUNNING_STATUSES.has(status) ? status : null; } function normalizeActiveStatus(status) { return status === "active" || status === "busy" || status === "processing" || status === "creating" ? "running" : status; } function terminalTraceEventEvidence(events = []) { const finalResponse = terminalAssistantEventFinalResponse(events); for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index]; if (!event || typeof event !== "object") continue; if (retryableProviderInterruptionEvidence(event)) continue; const terminal = event.terminal === true || event.final === true || event.replyAuthority === true; if (!terminal) continue; const status = terminalStatusFromValue(event.status ?? event.terminalStatus) ?? (event.error || event.errorCode ? "failed" : "completed"); return { source: "trace-terminal-event", status, seq: eventSeq(event, index), eventType: textValue(event.type ?? event.label) || null, finalResponse, evidence: finalResponse ? { textPresent: true, source: "trace-terminal-assistant-event", valuesRedacted: true } : null, valuesRedacted: true }; } return null; } function terminalAssistantEventFinalResponse(events = []) { for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index]; if (!event || typeof event !== "object") continue; if (!isAssistantTraceEvent(event)) continue; if (!(event.terminal === true || event.final === true || event.replyAuthority === true)) continue; const text = projectionText(event.finalResponse, event.text, event.content, event.message, event.summary, event.payload?.text, event.payload?.content, event.payload?.message); if (!text) continue; const status = terminalStatusFromValue(event.status ?? event.terminalStatus ?? event.payload?.terminalStatus) ?? "completed"; return { text, status, traceId: textValue(event.traceId) || null, seq: eventSeq(event, index), eventType: textValue(event.type ?? event.label) || null, valuesPrinted: false }; } return null; } function isAssistantTraceEvent(event = {}) { const type = String(event.type ?? event.eventType ?? "").trim().toLowerCase(); if (type === "assistant" || type === "assistant_message") return true; return /assistant:message|assistant_message/u.test(String(event.label ?? "").toLowerCase()); } function activeTraceEventStatus(events = []) { for (const event of [...events].reverse()) { if (retryableProviderInterruptionEvidence(event)) return "retrying"; const status = normalizeWorkbenchStatus(event?.status ?? event?.type); if (RUNNING_STATUSES.has(status)) return normalizeActiveStatus(status); } return null; } function retryableProviderInterruptionEvidence(...values) { for (const value of values) { const direct = retryableProviderInterruptionRecord(value); if (direct) return direct; } return null; } const RETRYABLE_AGENTRUN_TRANSPORT_FAILURE_KINDS = new Set([ "agentrun-connect-failed", "agentrun-proxy-exec-failed", "agentrun-manager-fetch-failed", "agentrun-timeout" ]); function isRetryableAgentRunTransportFailureKind(kind) { return RETRYABLE_AGENTRUN_TRANSPORT_FAILURE_KINDS.has(normalizedProjectionFailureKind(kind)); } function retryableProviderInterruptionRecord(value) { const record = objectValue(value); if (!record) return null; const kind = normalizedProjectionFailureKind(record.failureKind ?? record.errorCode ?? record.code ?? record.name); if ((kind === "provider-stream-disconnected" || kind === "provider-unavailable" || isRetryableAgentRunTransportFailureKind(kind)) && record.willRetry === true) { return { failureKind: kind, willRetry: true, retryAttempt: numberOrNull(record.retryAttempt), retryMax: numberOrNull(record.retryMax), retryDelayMs: numberOrNull(record.retryDelayMs), nextRetryAt: textValue(record.nextRetryAt) || null, runId: textValue(record.runId) || null, commandId: textValue(record.commandId) || null, valuesRedacted: true }; } for (const nested of [record.payload, record.providerTrace, record.agentRun, record.error, record.blocker, record.terminalEvidence]) { const found = retryableProviderInterruptionRecord(nested); if (found) return found; } return null; } function normalizedProjectionFailureKind(value) { const kind = normalizeWorkbenchStatus(value); if (kind === "agentrun-unreachable") return "agentrun-connect-failed"; return kind; } function hasTraceProjection(trace) { return Boolean(trace && trace.status !== "missing" && (normalizedEventCount(trace) > 0 || traceLastEvent(trace))); } function normalizedEventCount(trace) { const events = Array.isArray(trace?.events) ? trace.events : []; const count = Number(trace?.eventCount ?? events.length); return Number.isFinite(count) && count >= 0 ? Math.trunc(count) : events.length; } function traceLastSeq(trace) { const events = Array.isArray(trace?.events) ? trace.events : []; const lastEvent = traceLastEvent(trace); const indexedMax = events.reduce((max, event, index) => Math.max(max, eventSeq(event, index)), 0); return Math.max(indexedMax, lastEvent ? eventSeq(lastEvent, Math.max(0, events.length - 1)) : 0) || null; } function traceLastEvent(trace) { const events = Array.isArray(trace?.events) ? trace.events : []; return trace?.lastEvent ?? events.at(-1) ?? null; } function terminalTraceEventForTiming(events = []) { for (let index = events.length - 1; index >= 0; index -= 1) { const event = events[index]; if (!event || typeof event !== "object") continue; if (event.terminal === true || event.final === true || event.replyAuthority === true || terminalStatusFromValue(event.status ?? event.terminalStatus)) return event; } return null; } function firstTimestamp(...values) { for (const value of values) { const ms = Date.parse(String(value ?? "")); if (Number.isFinite(ms)) return new Date(ms).toISOString(); } return null; } function durationValue(...values) { let max = null; for (const value of values) { const number = Number(value); if (!Number.isFinite(number) || number < 0) continue; const duration = Math.trunc(number); max = max === null ? duration : Math.max(max, duration); } return max; } function elapsedBetween(startedAt, endedAt) { const start = Date.parse(String(startedAt ?? "")); const end = Date.parse(String(endedAt ?? "")); if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null; return end - start; } function projectionDiagnosticSource(trace) { const direct = objectValue(trace?.projection); if (direct) return direct; if (trace?.projectionHealth || trace?.projectionStatus || trace?.blocker) return trace; return null; } function normalizeProjectionStatus(value) { const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); return ["caught-up", "projecting", "blocked", "stalled", "unknown"].includes(text) ? text : null; } function normalizeProjectionHealth(value) { const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); return ["caught-up", "projecting", "degraded", "stalled", "unavailable", "unknown"].includes(text) ? text : null; } function projectionHealthFor({ status, turn, hasProjectionInput, blocker }) { if (blocker?.code === "projection_store_unavailable") return "unavailable"; if (blocker) return status === "stalled" ? "stalled" : "degraded"; if (turn.terminal || status === "caught-up") return "caught-up"; if (status === "stalled") return "stalled"; if (status === "projecting" || hasProjectionInput) return "projecting"; return "unknown"; } function projectionStaleMs(value, updatedAt) { const direct = Number(value); if (Number.isFinite(direct) && direct >= 0) return Math.trunc(direct); const updatedAtMs = Date.parse(String(updatedAt ?? "")); if (!Number.isFinite(updatedAtMs)) return null; return Math.max(0, Date.now() - updatedAtMs); } function eventSeq(event, index) { const seq = Number(event?.seq); return Number.isFinite(seq) && seq > 0 ? Math.trunc(seq) : index + 1; } function projectionText(...values) { for (const value of values) { if (value && typeof value === "object") { const nested = messageAuthorityTextValue(value.text ?? value.content ?? value.message ?? value.summary ?? value.preview ?? value.title); if (nested) return nested; continue; } const text = messageAuthorityTextValue(value); if (text) return text; } return null; } function objectValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function textValue(value) { return String(value ?? "").trim(); } function numberOrNull(value) { const number = Number(value); return Number.isFinite(number) ? number : null; } function messageAuthorityTextValue(value) { const text = String(value ?? "").replace(/\r\n?/gu, "\n"); if (!text.trim() || text.trim() === "[object Object]") return ""; return text; } function diagnosticBlocker(value = {}) { if (!value || typeof value !== "object") return { code: "projection_blocked", summary: String(value), valuesRedacted: true }; return { code: String(value.code ?? value.errorCode ?? "projection_blocked"), layer: textValue(value.layer ?? value.source) || null, category: textValue(value.category ?? value.type) || null, summary: String(value.summary ?? value.userMessage ?? value.message ?? value.code ?? "Projection is blocked."), message: textValue(value.message ?? value.summary ?? value.userMessage) || null, userMessage: textValue(value.userMessage ?? value.zh) || null, retryable: typeof value.retryable === "boolean" ? value.retryable : null, runId: textValue(value.runId) || null, commandId: textValue(value.commandId) || null, timeoutMs: Number.isFinite(Number(value.timeoutMs)) ? Math.trunc(Number(value.timeoutMs)) : null, valuesRedacted: true }; }