249 lines
8.4 KiB
TypeScript
249 lines
8.4 KiB
TypeScript
export const CODE_AGENT_SESSION_LIFECYCLE_STATUSES = Object.freeze([
|
|
"creating",
|
|
"ready",
|
|
"busy",
|
|
"idle",
|
|
"interrupted",
|
|
"expired",
|
|
"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(["interrupted", "expired", "failed"]);
|
|
|
|
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) && 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,
|
|
fallback: fallbackUsed,
|
|
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: "new_session",
|
|
userMessage: "当前 Code Agent session 已失败,需重新发送建立新会话。"
|
|
};
|
|
}
|
|
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;
|
|
}
|