Files
pikasTech-HWLAB/internal/cloud/code-agent-session-lifecycle.ts
T

257 lines
8.8 KiB
TypeScript

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;
}