Files
pikasTech-HWLAB/internal/cloud/code-agent-dispatch-failure.ts

138 lines
7.8 KiB
TypeScript

// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-07-12-p0-admission-failure-identity.
// Responsibility: Normalize typed AgentRun dispatch failures before durable admission.
// rollout 仅接受上游类型化 failureKind 作为权威;HTTP 状态、耗时和展示文案都不能推断 rollout。
const FAILURE_REASON_RULES = Object.freeze([
rule("rollout-in-progress", ["rollout-in-progress", "rollout-progressing", "deployment-in-progress", "runtime-starting", "service-starting"]),
rule("manager-service-unavailable", ["agentrun-connect-failed", "agentrun-manager-fetch-failed", "agentrun-request-failed", "agentrun-timeout", "agentrun-unreachable", "manager-unavailable", "service-unavailable", "upstream-unavailable", "http-502", "http-503", "http-504", "fetch-failed", "network-error", "timeout"]),
rule("admission-policy-denied", ["admission-denied", "policy-denied", "forbidden", "permission-denied", "rbac-denied", "unauthorized"]),
rule("runner-capacity-exhausted", ["runner-capacity", "runner-capacity-exhausted", "capacity-exhausted", "no-runner-capacity", "runner-unavailable", "runner-quota-exceeded"]),
rule("provider-profile-unconfigured", ["provider-profile-unconfigured", "provider-profile-missing", "provider-not-configured", "profile-not-configured", "provider-unconfigured"]),
rule("secret-unavailable", ["secret-unavailable", "secret-missing", "secret-not-found", "credential-unavailable", "credential-missing"]),
rule("image-unavailable", ["image-unavailable", "image-not-found", "image-pull-failed", "image-pull-backoff", "invalid-image"]),
rule("workspace-materialization-failed", ["workspace-materialization-failed", "resource-bundle-materialization-failed", "workspace-unavailable", "gitbundle-unavailable", "workspace-checkout-failed"]),
rule("lease-idempotency-conflict", ["lease-conflict", "lease-lost", "idempotency-conflict", "command-conflict", "dispatch-intent-conflict", "already-claimed"]),
rule("dispatcher-runtime-misconfigured", ["schema-invalid", "config-invalid", "configuration-invalid", "dispatcher-misconfigured", "runtime-misconfigured", "agentrun-internal-url-required", "invalid-dispatch-contract"])
]);
const FAILURE_PRESENTATIONS = Object.freeze({
"rollout-in-progress": {
userMessage: "AgentRun 正在滚动更新,本次请求尚未接纳;可稍后重试。",
recoveryAction: "wait-for-rollout"
},
"manager-service-unavailable": {
userMessage: "AgentRun 管理服务暂时不可用,本次请求尚未接纳;请等待服务恢复后重试。",
recoveryAction: "retry-when-agentrun-service-ready"
},
"admission-policy-denied": {
userMessage: "AgentRun 接纳策略拒绝了本次请求;请检查身份、租户与执行策略。",
recoveryAction: "review-admission-policy"
},
"runner-capacity-exhausted": {
userMessage: "AgentRun 当前没有可用 Runner 容量,本次请求尚未接纳;请释放或扩容后重试。",
recoveryAction: "release-or-add-runner-capacity"
},
"provider-profile-unconfigured": {
userMessage: "AgentRun Provider/Profile 未配置,本次请求无法接纳;请先完成对应 Profile 配置。",
recoveryAction: "configure-provider-profile"
},
"secret-unavailable": {
userMessage: "AgentRun 所需 Secret 不可用,本次请求无法接纳;请恢复 YAML 引用的 Secret。",
recoveryAction: "restore-referenced-secret"
},
"image-unavailable": {
userMessage: "AgentRun Runner 镜像不可用,本次请求无法接纳;请发布或修正目标镜像。",
recoveryAction: "publish-or-correct-runner-image"
},
"workspace-materialization-failed": {
userMessage: "AgentRun Workspace/Resource Bundle 物化失败,本次请求无法继续;请检查仓库与资源包引用。",
recoveryAction: "repair-workspace-materialization"
},
"lease-idempotency-conflict": {
userMessage: "AgentRun Lease/Idempotency 发生冲突;请核对已有 Run/Command 后再重试。",
recoveryAction: "inspect-existing-lease-or-idempotent-command"
},
"dispatcher-runtime-misconfigured": {
userMessage: "AgentRun 调度契约或运行面配置不匹配,本次请求未进入实时事件流;请展开诊断修复配置。",
recoveryAction: "repair-agentrun-dispatch-contract"
},
"unknown-dispatch-failure": {
userMessage: "AgentRun 返回了未识别的调度失败,本次请求尚未接纳;请展开诊断定位。",
recoveryAction: "inspect-agentrun-diagnostic"
}
});
export function classifyCodeAgentDispatchFailure(error = {}) {
const upstream = record(error?.agentRunError);
const nested = record(upstream?.error);
const failureKind = normalizedFailureKind(firstText(error?.failureKind, upstream?.failureKind, nested?.failureKind, error?.code, upstream?.code, nested?.code, "agentrun-dispatch-error"));
const statusCode = firstInteger(error?.statusCode, error?.status, upstream?.statusCode, upstream?.status, nested?.statusCode);
const reason = dispatchFailureReason(failureKind, statusCode);
const presentation = FAILURE_PRESENTATIONS[reason] ?? FAILURE_PRESENTATIONS["unknown-dispatch-failure"];
const retryable = firstBoolean(error?.retryable, upstream?.retryable, nested?.retryable, defaultRetryable(reason, statusCode));
return {
failureKind,
reason,
retryable,
userMessage: presentation.userMessage,
recoveryAction: presentation.recoveryAction,
message: firstText(error?.message, upstream?.message, nested?.message, "AgentRun dispatch failed before durable admission."),
statusCode,
dispatchStage: firstText(error?.dispatchStage, error?.stage, upstream?.dispatchStage, upstream?.stage),
upstreamTraceId: firstText(upstream?.traceId, nested?.traceId),
runId: firstText(error?.runId, upstream?.runId, upstream?.run?.id, nested?.runId),
commandId: firstText(error?.commandId, upstream?.commandId, upstream?.command?.id, nested?.commandId),
dispatchIntentId: firstText(error?.dispatchIntentId, upstream?.dispatchIntentId, upstream?.dispatchIntent?.id, nested?.dispatchIntentId),
valuesPrinted: false
};
}
export function dispatchFailureReason(failureKind, statusCode = null) {
const normalized = normalizedFailureKind(failureKind);
const matched = FAILURE_REASON_RULES.find((candidate) => candidate.kinds.has(normalized));
if (matched) return matched.reason;
if (statusCode === 401 || statusCode === 403) return "admission-policy-denied";
if (statusCode === 409) return "lease-idempotency-conflict";
if ([502, 503, 504].includes(statusCode)) return "manager-service-unavailable";
return "unknown-dispatch-failure";
}
function rule(reason, kinds) {
return Object.freeze({ reason, kinds: new Set(kinds) });
}
function defaultRetryable(reason, statusCode) {
if (["rollout-in-progress", "manager-service-unavailable", "runner-capacity-exhausted", "lease-idempotency-conflict"].includes(reason)) return true;
if (["admission-policy-denied", "provider-profile-unconfigured", "secret-unavailable", "image-unavailable", "workspace-materialization-failed", "dispatcher-runtime-misconfigured"].includes(reason)) return false;
return Number.isInteger(statusCode) && statusCode >= 500;
}
function normalizedFailureKind(value) {
const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-").replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, "");
return text || "agentrun-dispatch-error";
}
function record(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function firstText(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return null;
}
function firstInteger(...values) {
for (const value of values) {
const number = Number(value);
if (Number.isInteger(number) && number > 0) return number;
}
return null;
}
function firstBoolean(...values) {
for (const value of values) if (typeof value === "boolean") return value;
return false;
}