export const CODE_AGENT_SESSION_LIFECYCLE_STATUSES = Object.freeze([ "creating", "ready", "busy", "idle", "interrupted", "expired", "failed", "thread-resume-failed" ]); export const CODE_AGENT_SESSION_STATUS_ALIASES = Object.freeze({ canceled: "interrupted", cancelled: "interrupted", timeout: "failed", error: "failed", blocked: "failed", degraded: "failed" }); const ACTIVE_LIFECYCLE_STATUSES = new Set(["creating", "ready", "busy", "idle"]); const TERMINAL_LIFECYCLE_STATUSES = new Set(["expired", "thread-resume-failed"]); const RECOVERABLE_LIFECYCLE_STATUSES = new Set(["failed", "interrupted"]); export function normalizeCodeAgentSessionLifecycleStatus(value, { fallback = null } = {}) { const text = String(value ?? "").trim().toLowerCase(); if (!text) return fallback; if (CODE_AGENT_SESSION_LIFECYCLE_STATUSES.includes(text)) return text; if (CODE_AGENT_SESSION_STATUS_ALIASES[text]) return CODE_AGENT_SESSION_STATUS_ALIASES[text]; if (text === "running" || text === "in_flight" || text === "in-flight") return "busy"; if (text === "completed" || text === "available" || text === "feasible" || text === "pass") return "idle"; return fallback; } export function codeAgentSessionLifecycleSummary(input = {}) { const session = objectOrNull(input.session); const runnerTrace = objectOrNull(input.runnerTrace); const sessionReuse = objectOrNull(input.sessionReuse); const sessionSummary = objectOrNull(input.sessionSummary); const evidenceSessionId = firstText( session?.sessionId, sessionReuse?.sessionId, runnerTrace?.sessionId ); const topLevelSessionId = firstText(input.sessionId); const conversationId = firstText(input.conversationId); const hasSession = Boolean(evidenceSessionId || (topLevelSessionId && topLevelSessionId !== conversationId)); const rawStatus = firstText( input.lifecycleStatus, input.sessionLifecycleStatus, session?.lifecycleStatus, session?.lifecycle?.status, sessionSummary?.status, session?.status, input.sessionStatus, runnerTrace?.sessionLifecycleStatus, runnerTrace?.sessionStatus, sessionReuse?.status, input.status ); const fallback = fallbackLifecycleStatus(input); let status = normalizeCodeAgentSessionLifecycleStatus(rawStatus, { fallback }); const reused = booleanOrNull(input.reused) ?? booleanOrNull(sessionSummary?.reused) ?? booleanOrNull(sessionReuse?.reused) ?? booleanOrNull(session?.reused) ?? booleanOrNull(runnerTrace?.sessionReused); const fallbackUsed = isFallbackPayload(input); const concreteLifecycleBlock = ["busy", "expired", "failed", "interrupted"].includes(status) && hasSession; const gateBlocked = objectOrNull(input.longLivedSessionGate)?.status === "blocked"; const evidenceMissing = objectOrNull(input.blocker)?.code === "code_agent_session_evidence_missing" || objectOrNull(input.error)?.code === "code_agent_session_evidence_missing"; const degraded = fallbackUsed || input.unsupported === true || input.degraded === true || evidenceMissing || (gateBlocked && !concreteLifecycleBlock); if ((fallbackUsed || degraded) && !hasSession) status = "failed"; const requiresNewSession = status ? TERMINAL_LIFECYCLE_STATUSES.has(status) : degraded || fallbackUsed; const reusable = status ? (ACTIVE_LIFECYCLE_STATUSES.has(status) || RECOVERABLE_LIFECYCLE_STATUSES.has(status)) && status !== "creating" && degraded !== true : false; const newSession = reused === false || (hasSession && !reused && !requiresNewSession); const reasonCode = firstText( input.degradationReasonCode, objectOrNull(input.error)?.code, objectOrNull(input.blocker)?.code, objectOrNull(input.longLivedSessionGate)?.blocker, fallbackUsed ? "text_chat_only_fallback" : null ); const reason = firstText( input.degradationReason, objectOrNull(input.error)?.userMessage, objectOrNull(input.blocker)?.userMessage, objectOrNull(input.blocker)?.summary, objectOrNull(input.error)?.message, reasonCode ); const presentation = lifecyclePresentation({ status, rawStatus, reused, newSession, degraded, fallbackUsed, reason }); return { status, rawStatus: rawStatus ?? null, label: presentation.label, action: presentation.action, reusable, reused, newSession, busy: status === "busy", requiresNewSession, degraded, unsupported: input.unsupported === true, reasonCode: reasonCode ?? null, reason: reason ?? null, userMessage: presentation.userMessage }; } export function decorateCodeAgentSession(session, context = {}) { if (!session || typeof session !== "object") return session; const lifecycle = codeAgentSessionLifecycleSummary({ ...context, session }); return { ...session, lifecycleStatus: lifecycle.status, lifecycle }; } function fallbackLifecycleStatus(input) { if (isFallbackPayload(input) || input.degraded === true || input.unsupported === true) return "failed"; const status = String(input.status ?? "").toLowerCase(); if (status === "running") return "busy"; if (["failed", "timeout", "error"].includes(status)) return "failed"; if (status === "canceled") return "interrupted"; return null; } function lifecyclePresentation({ status, rawStatus, reused, newSession, degraded, fallbackUsed, reason }) { if (fallbackUsed) { return { label: "会话降级", action: "degraded", userMessage: "当前响应来自 fallback/text-only/read-only 降级路径,不会按完整 Code Agent 长会话成功展示。" }; } if (degraded) { return { label: "会话降级", action: "degraded", userMessage: `当前会话证据不完整或能力降级;输入已保留,可重新发送建立新会话。${reason ? ` 原因:${reason}` : ""}` }; } if (status === "busy") { return { label: "会话忙", action: "wait", userMessage: "会话忙/请等待:上一轮仍在处理,稍后重试或取消当前请求。" }; } if (status === "expired") { return { label: "会话已过期", action: "new_session", userMessage: "会话已过期,可重新发送建立新的 Code Agent session。" }; } if (status === "failed") { return { label: "会话失败", action: "retry", userMessage: "当前 Code Agent session 上一轮失败;可继续发送以尝试恢复同一 conversation/session/thread。" }; } if (status === "thread-resume-failed") { return { label: "会话存储已失效", action: "new_session", userMessage: "当前 Code Agent session/thread 的后端存储已失效;下一轮应从新的 session/thread 继续。" }; } if (status === "interrupted") { return { label: rawStatus === "canceled" ? "会话已取消" : "会话已中断", action: "retry", userMessage: "当前 Code Agent session 已中断;输入和 trace 已保留,可重试或重新发送。" }; } if (status === "creating") { return { label: "正在建立会话", action: "wait", userMessage: "正在建立 Code Agent session,请等待后端返回 session/thread。" }; } if (status === "ready" || status === "idle") { if (reused === true) { return { label: "继续当前会话", action: "continue", userMessage: "可继续当前会话:后端已确认复用现有 conversation/session/thread。" }; } if (newSession === true) { return { label: "新会话已建立", action: "continue", userMessage: "新会话已建立:后端返回了可复用的 session/thread。" }; } return { label: "会话可用", action: "continue", userMessage: "Code Agent session 处于可继续状态。" }; } return { label: "会话未确认", action: "observe", userMessage: "尚未观测到可复用的 Code Agent session 状态。" }; } function isFallbackPayload(input) { const runner = objectOrNull(input.runner); const error = objectOrNull(input.error); const blocker = objectOrNull(input.blocker ?? error?.blocker); return input.fallback === true || input.fallbackUsed === true || input.capabilityLevel === "text-chat-only" || input.provider === "openai-responses" || runner?.kind === "openai-responses-fallback" || input.implementationType === "openai-responses-fallback" || blocker?.code === "text_chat_only_fallback" || error?.code === "text_chat_only_fallback"; } function objectOrNull(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function firstText(...values) { for (const value of values) { const text = String(value ?? "").trim(); if (text) return text; } return null; } function booleanOrNull(value) { return typeof value === "boolean" ? value : null; }