Files
pikasTech-HWLAB/web/hwlab-cloud-web/code-agent-status.mjs
T
Lyon 034c90763c fix: align Code Agent session facts
Align Code Agent chat, live health, and workbench UI session facts for Codex stdio vs fallback paths.
2026-05-24 07:34:27 +08:00

420 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
runnerTraceSummary as compactRunnerTraceSummary,
skillsSummary as compactSkillsSummary,
toolCallsSummary as compactToolCallsSummary
} from "./code-agent-facts.mjs";
const GREEN_TONES = new Set(["ok", "available", "ready", "dev-live", "live", "pass"]);
const SECRET_MARKER = /\b(?:OPENAI_API_KEY|DATABASE_URL|secret|secretref|token|password|passwd|credential|private key|kubeconfig|密钥|密码|凭证)\b/iu;
const NON_LIVE_VALUES = new Set(["", "n/a", "none", "null", "undefined", "unknown", "not_observed", "not-observed"]);
const MAX_FIELD_LENGTH = 96;
export function classifyCodeAgentStatusSummary({ availability = null, latestMessage = null, live = null } = {}) {
const observedAvailability = availability ?? codeAgentAvailabilityFromLive(live);
const payload = safeMerge(observedAvailability, latestMessage);
const blockers = readinessBlockers(payload);
const classification = classifyPayload(payload, blockers);
const safeTone = unsafeGreenForNonReady(classification, blockers) ? "warn" : classification.tone;
const provider = safeField(firstString(payload.provider, observedAvailability?.provider));
const mode = safeField(firstString(payload.mode, payload.providerTrace?.mode, observedAvailability?.mode));
const backend = safeField(firstString(payload.backend, observedAvailability?.backend));
const codeAgentStatus = safeField(firstString(payload.status, payload.readinessStatus, observedAvailability?.status));
const capabilityLevel = safeField(firstString(payload.capabilityLevel, payload.runner?.capabilityLevel, observedAvailability?.capabilityLevel));
const sessionMode = safeField(firstString(payload.sessionMode, payload.session?.sessionMode, payload.runnerTrace?.sessionMode, payload.runner?.sessionMode));
const sessionId = safeField(firstString(payload.session?.sessionId, payload.sessionId, payload.runner?.sessionId, payload.sessionReuse?.sessionId));
const sessionStatus = safeField(firstString(payload.session?.status, payload.sessionStatus, payload.runnerTrace?.sessionStatus, payload.sessionReuse?.status));
const workspace = safeField(firstString(payload.workspace, payload.session?.workspace, payload.runner?.workspace, payload.sessionReuse?.workspace));
const sandbox = safeField(firstString(payload.sandbox, payload.session?.sandbox, payload.runner?.sandbox));
const runnerKind = safeField(firstString(payload.runner?.kind, payload.session?.runnerKind, payload.runnerTrace?.runnerKind));
const toolCalls = safeField(compactToolCallsSummary(payload.toolCalls));
const skills = safeField(compactSkillsSummary(payload.skills));
const runnerTrace = safeField(compactRunnerTraceSummary(payload.runnerTrace));
const lastTraceId = safeTraceId(firstString(
latestMessage?.traceId,
payload.traceId,
payload.session?.lastTraceId,
payload.runnerTrace?.lastTraceId,
payload.runnerTrace?.traceId,
payload.providerTrace?.traceId,
observedAvailability?.traceId
));
const deploymentRevision = currentDeploymentRevision(live);
return {
...classification,
tone: safeTone,
provider,
mode,
backend,
providerModeBackend: compactJoin([provider, mode, backend], " / "),
codeAgentStatus,
capabilityLevel,
sessionMode,
sessionId,
sessionStatus,
sessionSummary: compactJoin([sessionMode, sessionStatus], " / "),
sessionIdStatus: compactJoin([sessionId, sessionStatus], " / "),
workspace,
sandbox,
runnerKind,
toolCalls,
skills,
runnerTrace,
readinessBlockers: blockers,
blockersLabel: blockers.length > 0 ? blockers.join(", ") : "无",
lastTraceId: lastTraceId || "未观测",
deploymentRevision,
fields: {
provider,
mode,
backend,
codeAgentStatus,
capabilityLevel,
sessionMode,
sessionId,
sessionStatus,
workspace,
sandbox,
runnerKind,
toolCalls,
skills,
runnerTrace,
readinessBlockers: blockers,
lastTraceId: lastTraceId || null,
deploymentRevision
}
};
}
export function codeAgentAvailabilityFromLive(live = {}) {
return normalizedCodeAgentAvailability(
live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? live?.health?.data?.codeAgent ?? null,
live?.restIndex?.data?.readiness?.codeAgent ?? live?.healthLive?.data?.readiness?.codeAgent ?? live?.health?.data?.readiness?.codeAgent ?? null
);
}
export function currentDeploymentRevision(live = {}) {
const payloads = [
live?.healthLive?.data,
live?.restIndex?.data,
live?.health?.data,
live?.adapter?.data
].filter(Boolean);
for (const payload of payloads) {
const revision = firstString(
payload?.commit?.id,
payload?.commitId,
payload?.revision,
payload?.runtime?.commitId,
payload?.image?.tag,
payload?.imageTag,
payload?.runtime?.imageTag
);
if (revision && !NON_LIVE_VALUES.has(revision.toLowerCase())) {
return shortRevision(revision);
}
}
return "未观测";
}
function classifyPayload(payload, blockers) {
if (!payload || Object.keys(payload).length === 0) {
return status("unverified", "探测中", "pending", "○", "等待同源接口返回 Code Agent 状态。");
}
if (isSkillCliApiControl(payload)) {
const ready = isReadySkillCli(payload);
return ready
? status("skill-cli-api-control", "HWLAB Skill CLI 控制", "ok", "◆", "通过 HWLAB API 受控路径执行指定能力。")
: status("skill-cli-api-blocked", "Skill CLI 受阻", "blocked", "×", "HWLAB Skill CLI 控制路径当前受阻。");
}
if (isLongLivedReady(payload)) {
return status("long-lived-session", "长会话可用", "ok", "●", "repo-owned 长会话通道可复用。");
}
if (isReadOnlySessionTools(payload)) {
return status("read-only-session-tools", "只读长会话工具", "warn", "◇", "只读 runner 会话可复用,不能写工作区或冒充完整 Codex stdio。");
}
if (isStatelessOneShot(payload)) {
return status("stateless-one-shot", "一次性执行", "warn", "△", "当前回复来自一次性执行,不是可复用长会话。");
}
if (isBlocked(payload)) {
return status("blocked", "服务受阻", "blocked", "×", "Code Agent 通道暂不可用或 readiness 未通过。");
}
if (isFallbackTextChatOnly(payload)) {
return status("fallback-text-chat-only", "文本备用通道", "warn", "◌", "只有文本对话能力,没有 workspace tools 或 session 控制。");
}
if (isDegraded(payload, blockers)) {
return status("degraded", "降级可用", "warn", "!", "Code Agent 有部分能力,但 readiness 仍有阻塞项。");
}
if (payload.status === "available" || payload.ready === true || payload.status === "completed") {
return status("available", "通道可用", "ok", "●", "受控对话通道已返回可用状态。");
}
return status("unverified", "探测中", "pending", "○", "等待同源接口返回 Code Agent 状态。");
}
function normalizedCodeAgentAvailability(availability, structuredReadiness) {
const base = availability && typeof availability === "object" ? { ...availability } : {};
const structured = structuredReadiness && typeof structuredReadiness === "object" ? structuredReadiness : null;
if (!structured) return Object.keys(base).length > 0 ? base : null;
const provider = structured.provider ?? {};
const sessionRunner = structured.sessionRunner ?? {};
const codexStdio = structured.codexStdio ?? {};
const merged = {
...base,
structuredReadiness: structured,
status: firstString(base.status, structured.status),
ready: base.ready === true || structured.ready === true,
readinessStatus: firstString(base.readinessStatus, structured.status),
providerReady: structured.providerReady === true,
durableDbReady: structured.durableDbReady === true,
sessionRunnerReady: structured.sessionRunnerReady === true,
codexStdioFeasible: structured.codexStdioFeasible === true,
provider: firstString(base.provider, provider.provider),
mode: firstString(base.mode, provider.mode, sessionRunner.mode),
backend: firstString(base.backend, provider.backend),
capabilityLevel: firstString(base.capabilityLevel, sessionRunner.capabilityLevel),
sessionMode: firstString(base.sessionMode, sessionRunner.mode),
currentBlockers: Array.isArray(structured.currentBlockers) ? [...structured.currentBlockers] : [],
blockerCodes: [
...(Array.isArray(structured.currentBlockers) ? structured.currentBlockers : []),
...(Array.isArray(codexStdio.blockerCodes) ? codexStdio.blockerCodes : [])
],
blockers: [
...(Array.isArray(structured.currentBlockers)
? structured.currentBlockers.map((code) => ({ code }))
: [])
],
runner: {
...(base.runner ?? {}),
kind: firstString(base.runner?.kind, sessionRunner.kind),
provider: firstString(base.runner?.provider, provider.provider),
backend: firstString(base.runner?.backend, provider.backend),
sessionMode: firstString(base.runner?.sessionMode, sessionRunner.mode),
capabilityLevel: firstString(base.runner?.capabilityLevel, sessionRunner.capabilityLevel),
longLivedSession: base.runner?.longLivedSession === true || sessionRunner.longLivedSession === true,
durableSession: base.runner?.durableSession === true || sessionRunner.durableSession === true,
codexStdio: base.runner?.codexStdio === true || sessionRunner.codexStdio === true,
writeCapable: base.runner?.writeCapable === true || sessionRunner.writeCapable === true
},
codexStdioFeasibility: {
...(base.codexStdioFeasibility ?? base.codexStdio ?? {}),
ready: codexStdio.ready === true,
canStartLongLivedCodexStdio: codexStdio.feasible === true || codexStdio.ready === true,
status: codexStdio.status,
blockerCodes: Array.isArray(codexStdio.blockerCodes) ? [...codexStdio.blockerCodes] : [],
blockers: codexStdio.blocker ? [{ code: codexStdio.blocker }] : []
}
};
if (structured.currentBlocker) merged.reason = firstString(base.reason, structured.currentBlocker);
return merged;
}
function status(kind, label, tone, icon, summary) {
return { kind, label, tone, icon, summary };
}
function safeMerge(availability, latestMessage) {
const base = availability && typeof availability === "object" ? availability : {};
const message = latestMessage && typeof latestMessage === "object" ? latestMessage : {};
const merged = {
...base,
...message,
runner: { ...(base.runner ?? {}), ...(message.runner ?? {}) },
session: mergeSession(base.session, message.session),
runnerTrace: { ...(base.runnerTrace ?? {}), ...(message.runnerTrace ?? {}) },
providerTrace: { ...(base.providerTrace ?? {}), ...(message.providerTrace ?? {}) },
longLivedSessionGate: { ...(base.longLivedSessionGate ?? {}), ...(message.longLivedSessionGate ?? {}) },
codexStdioFeasibility: { ...(base.codexStdioFeasibility ?? base.codexStdio ?? {}), ...(message.codexStdioFeasibility ?? {}) }
};
if (base.blockers || message.blockers) merged.blockers = [...(base.blockers ?? []), ...(message.blockers ?? [])];
if (base.blockerCodes || message.blockerCodes) merged.blockerCodes = [...(base.blockerCodes ?? []), ...(message.blockerCodes ?? [])];
if (base.runnerLimitations || message.runnerLimitations) merged.runnerLimitations = [...(base.runnerLimitations ?? []), ...(message.runnerLimitations ?? [])];
if (message.toolCalls !== undefined) merged.toolCalls = message.toolCalls;
if (message.skills !== undefined) merged.skills = message.skills;
return merged;
}
function mergeSession(baseSession, messageSession) {
const base = typeof baseSession === "string" ? { status: baseSession } : (baseSession ?? {});
const message = typeof messageSession === "string" ? { status: messageSession } : (messageSession ?? {});
return { ...base, ...message };
}
function isSkillCliApiControl(payload) {
return (
payload.provider === "hwlab-skill-cli" ||
payload.runner?.kind === "hwlab-m3-io-skill-cli" ||
payload.sessionMode === "controlled-m3-io-skill-cli" ||
String(payload.capabilityLevel ?? "").startsWith("hwlab-api-control") ||
array(payload.toolCalls).some((tool) => tool?.type === "skill-cli" || tool?.route === "/v1/m3/io")
);
}
function isReadySkillCli(payload) {
return (
payload.capabilityLevel === "hwlab-api-control-ready" ||
payload.capabilityLevel === "hwlab-api-control-with-approval" ||
payload.providerTrace?.controlReady === true ||
array(payload.toolCalls).some((tool) => tool?.controlReady === true && tool?.status === "completed")
) && !hasBlockingSkillCliSignal(payload);
}
function hasBlockingSkillCliSignal(payload) {
return payload.capabilityLevel === "hwlab-api-control-blocked" ||
payload.providerTrace?.controlReady === false ||
array(payload.toolCalls).some((tool) => tool?.status === "blocked" || tool?.controlReady === false);
}
function isLongLivedReady(payload) {
return (
(
payload.longLivedSessionGate?.status === "pass" ||
payload.longLivedSessionGate?.pass === true ||
payload.codexStdioFeasibility?.canStartLongLivedCodexStdio === true
) &&
(
payload.provider === "codex-stdio" ||
payload.runner?.kind === "codex-mcp-stdio-runner" ||
payload.runner?.codexStdio === true ||
payload.session?.codexStdio === true
) &&
payload.capabilityLevel === "long-lived-codex-stdio-session" &&
payload.sessionMode === "codex-mcp-stdio-long-lived"
) && payload.status !== "failed" && payload.status !== "blocked" && payload.ready !== false;
}
function isReadOnlySessionTools(payload) {
return (
payload.capabilityLevel === "read-only-session-tools" ||
payload.capabilityLevel === "read-only-tools" ||
payload.agentKind === "controlled-readonly-session-registry" ||
payload.sessionMode === "controlled-readonly-session-registry" ||
payload.implementationType === "controlled-readonly-session-registry" ||
payload.runner?.kind === "hwlab-readonly-runner" ||
(payload.runner?.writeCapable === false &&
(payload.runner?.ready === true || payload.partialReady === true || payload.sessionRunnerReady === true) &&
payload.capabilityLevel !== "text-chat-only" &&
payload.runner?.kind !== "openai-responses-fallback") ||
payload.partialReady === true
);
}
function isStatelessOneShot(payload) {
return (
payload.sessionMode === "ephemeral-one-shot" ||
payload.runner?.kind === "codex-cli-one-shot-ephemeral" ||
payload.implementationType === "codex-cli-one-shot-ephemeral" ||
array(payload.runnerLimitations).includes("one-shot")
);
}
function isFallbackTextChatOnly(payload) {
return (
payload.capabilityLevel === "text-chat-only" ||
payload.runner?.kind === "openai-responses-fallback" ||
payload.implementationType === "openai-responses-fallback" ||
payload.agentKind === "openai-fallback" ||
payload.provider === "openai-responses"
);
}
function isBlocked(payload) {
return payload.status === "blocked" ||
payload.status === "failed" ||
payload.ready === false ||
payload.capabilityLevel === "blocked" ||
Boolean(payload.error);
}
function isDegraded(payload, blockers) {
return payload.status === "partial" ||
payload.status === "degraded" ||
payload.readinessStatus === "partial" ||
payload.capabilityStatus === "controlled-readonly-session-registry" ||
blockers.length > 0;
}
function readinessBlockers(payload) {
const values = [
...array(payload?.blockerCodes),
...array(payload?.blockers).map((item) => item?.code ?? item),
...array(payload?.longLivedSessionGate?.blockers).map((item) => item?.code ?? item),
...array(payload?.codexStdioFeasibility?.blockers).map((item) => item?.code ?? item),
payload?.runtimeContract?.binary?.status === "missing" ? "codex_cli_binary_missing" : null,
payload?.runtimeContract?.binary?.present === true && payload?.runtimeContract?.binary?.executable === false ? "codex_cli_not_executable" : null,
payload?.runtimeContract?.binary?.present === true && payload?.runtimeContract?.binary?.nativeDependencyPresent === false ? "codex_cli_native_dependency_missing" : null,
payload?.runtimeContract?.lifecycleSupervisor?.status === "blocked" ? "runner_lifecycle_missing" : null,
payload?.runtimeContract?.stdioProtocol?.status === "blocked" ? "stdio_protocol_not_wired" : null,
payload?.reason,
payload?.error?.code,
payload?.providerStatus ? `provider_http_${payload.providerStatus}` : null,
payload?.error?.providerStatus ? `provider_http_${payload.error.providerStatus}` : null,
...array(payload?.missingEnv).map(() => "provider_config_blocked")
];
return [...new Set(values.map(safeBlocker).filter(Boolean))].slice(0, 4);
}
function safeBlocker(value) {
if (value && typeof value === "object" && !Array.isArray(value) && !value.code && !value.reason && !value.message) return null;
const text = safeField(typeof value === "object" ? value?.code ?? value?.reason ?? value?.message : value);
if (!text || text === "未观测" || /^_+$/u.test(text)) return null;
if (SECRET_MARKER.test(text)) return "provider_config_blocked";
return text.replace(/[^A-Za-z0-9_.:-]/gu, "_").slice(0, 72);
}
function unsafeGreenForNonReady(classification, blockers) {
return GREEN_TONES.has(classification.tone) &&
(classification.kind.includes("blocked") || classification.kind === "degraded" || blockers.some((blocker) => /blocked|unavailable|failed|degraded/iu.test(blocker)));
}
function currentString(value) {
const text = String(value ?? "").trim();
if (!text || NON_LIVE_VALUES.has(text.toLowerCase())) return null;
return text;
}
function firstString(...values) {
for (const value of values) {
const text = currentString(value);
if (text) return text;
}
return null;
}
function safeField(value) {
const text = currentString(value);
if (!text) return "未观测";
if (SECRET_MARKER.test(text)) return "已隐藏";
return text.replace(/\s+/gu, " ").slice(0, MAX_FIELD_LENGTH);
}
function safeTraceId(value) {
const text = currentString(value);
if (!text || SECRET_MARKER.test(text)) return null;
return text.replace(/\s+/gu, "").slice(0, MAX_FIELD_LENGTH);
}
function shortRevision(value) {
const text = safeTraceId(value);
if (!text) return "未观测";
return /^[a-f0-9]{12,40}$/iu.test(text) ? text.slice(0, 12) : text.slice(0, 24);
}
function compactJoin(values, separator) {
const parts = values.map((value) => value === "未观测" ? null : value).filter(Boolean);
return parts.length > 0 ? parts.join(separator) : "未观测";
}
function array(value) {
return Array.isArray(value) ? value : [];
}