138 lines
7.8 KiB
TypeScript
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;
|
|
}
|