fix: add structured Code Agent blockers

Add structured Code Agent blocker taxonomy across /v1/agent/chat, runner/session/tool, and HWLAB M3 Skill CLI paths.\n\nValidated with npm run check and focused server/session/Skill CLI/web tests.
This commit is contained in:
Lyon
2026-05-23 23:17:13 +08:00
committed by GitHub
parent e86cbecd1d
commit 997b96dc65
10 changed files with 1104 additions and 101 deletions
@@ -12,6 +12,10 @@ const blockedCodeAgentUiLabels = new Set([
"Session 受阻",
"Runner 受阻",
"API 错误",
"需要配置",
"能力未开放",
"安全阻断",
"仍是 fallback",
"BLOCKED 凭证缺口"
]);
const timeoutPattern = /\b(?:timeout|timed out|abort|aborted|超时|请求超过)\b/iu;
@@ -311,6 +315,7 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId
traceId: observedTraceId,
hasReply: false,
error: blockedError,
blocker: summarizeBlocker(payload.blocker ?? payload.error?.blocker),
runner: summarizeRunner(payload),
workspace: stringOrNull(payload.workspace),
sandbox: stringOrNull(payload.sandbox),
@@ -338,6 +343,7 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId
traceId: observedTraceId,
hasReply: assistantReply.length > 0,
error,
blocker: summarizeBlocker(payload.blocker ?? payload.error?.blocker),
runner: summarizeRunner(payload),
workspace: stringOrNull(payload.workspace),
sandbox: stringOrNull(payload.sandbox),
@@ -406,8 +412,55 @@ export function classifyCodeAgentBrowserJourney({ responseSummary, httpOk = fals
export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {}) {
const errorCode = stringOrNull(payload?.error?.code);
const blockerCode = stringOrNull(payload?.blocker?.code ?? payload?.error?.blocker?.code);
const layer = stringOrNull(payload?.error?.layer ?? payload?.blocker?.layer ?? payload?.error?.blocker?.layer);
const category = stringOrNull(payload?.error?.category ?? payload?.blocker?.category ?? payload?.error?.blocker?.category);
const payloadStatus = stringOrNull(payload?.status);
if (["skill_cli_api_base_missing"].includes(errorCode ?? blockerCode)) {
return {
blocked: true,
status: "blocked",
blocker: "needs-config",
category: "needs_config",
layer,
reason: "M3 Skill CLI is missing a safe HWLAB API base URL contract; do not show generic send failure."
};
}
if (["hwlab_api_unavailable"].includes(errorCode ?? blockerCode)) {
return {
blocked: true,
status: "blocked",
blocker: "retryable",
category: "retryable",
layer,
reason: "HWLAB API is unavailable; the UI should show retryable blocker semantics."
};
}
if (["text_chat_only_fallback"].includes(errorCode ?? blockerCode) || category === "fallback") {
return {
blocked: true,
status: "blocked",
blocker: "text-chat-only-fallback",
category: "fallback",
layer,
reason: "Code Agent response is still text chat fallback and does not satisfy runner/session/tool control."
};
}
if (["m3_readiness_blocked"].includes(errorCode ?? blockerCode) || layer === "m3-readiness") {
return {
blocked: true,
status: "blocked",
blocker: "capability-unavailable",
category: category ?? "capability_unavailable",
layer,
reason: "M3 readiness is blocked; the UI must not treat this as generic send failure or completed control."
};
}
if (errorCode === "session_busy") {
return {
blocked: true,
@@ -464,6 +517,15 @@ export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {
export function classifyCodeAgentBrowserFailure(error, { traceId = null } = {}) {
const message = error instanceof Error ? error.message : String(error ?? "");
const observedTraceId = traceId ?? error?.traceId ?? null;
if (error?.userMessage) {
return {
status: "blocked",
blocker: blockerForStructuredCategory(error.category, error.code),
category: error.category ?? "runner_blocked",
chineseSummary: `${error.userMessage}${observedTraceId ? `traceId=${observedTraceId}` : ""}`,
reason: message
};
}
if (timeoutPattern.test(message)) {
return {
status: "blocked",
@@ -518,6 +580,18 @@ export function classifyCodeAgentBrowserFailure(error, { traceId = null } = {})
};
}
function blockerForStructuredCategory(category, code) {
if (category === "needs_config") return "needs-config";
if (category === "security_blocked" || code === "security_blocked") return "security-blocked";
if (category === "fallback" || code === "text_chat_only_fallback") return "text-chat-only-fallback";
if (category === "capability_unavailable") return "capability-unavailable";
if (category === "retryable") return "retryable";
if (category === "runner_busy") return "runner-busy";
if (category === "session_blocked") return "session-blocked";
if (category === "timeout") return "timeout";
return "runtime";
}
export function inspectRealCompletionEvidence(payload) {
const provider = String(payload?.provider ?? "").trim().toLowerCase();
const backend = String(payload?.backend ?? "").trim().toLowerCase();
@@ -559,6 +633,15 @@ export function normalizeCodeAgentError(error) {
if (!error || typeof error !== "object") return null;
return {
code: stringOrNull(error.code),
layer: stringOrNull(error.layer),
category: stringOrNull(error.category),
retryable: error.retryable === true,
userMessage: stringOrNull(error.userMessage),
traceId: stringOrNull(error.traceId),
route: stringOrNull(error.route),
toolName: stringOrNull(error.toolName),
blocker: summarizeBlocker(error.blocker),
missingConfig: Array.isArray(error.missingConfig) ? error.missingConfig.filter((item) => typeof item === "string") : [],
missingEnv: Array.isArray(error.missingEnv) ? error.missingEnv.filter((item) => typeof item === "string") : [],
missingCommands: Array.isArray(error.missingCommands) ? error.missingCommands.filter((item) => typeof item === "string") : [],
providerStatus: numericStatus(error.providerStatus)
@@ -618,6 +701,9 @@ function summarizeToolCalls(value) {
name: stringOrNull(tool?.name),
type: stringOrNull(tool?.type),
status: stringOrNull(tool?.status),
route: stringOrNull(tool?.route),
capabilityLevel: stringOrNull(tool?.capabilityLevel),
blocker: summarizeBlocker(tool?.blocker),
exitCode: numericStatus(tool?.exitCode),
outputTruncated: tool?.outputTruncated === true
}));
@@ -642,6 +728,9 @@ function summarizeRunnerTrace(value) {
if (!value || typeof value !== "object") return null;
return {
runnerKind: stringOrNull(value.runnerKind),
route: stringOrNull(value.route),
capabilityLevel: stringOrNull(value.capabilityLevel),
blocker: summarizeBlocker(value.blocker),
sandbox: stringOrNull(value.sandbox),
sessionMode: stringOrNull(value.sessionMode),
implementationType: stringOrNull(value.implementationType),
@@ -652,6 +741,19 @@ function summarizeRunnerTrace(value) {
};
}
function summarizeBlocker(value) {
if (!value || typeof value !== "object") return null;
return {
code: stringOrNull(value.code),
layer: stringOrNull(value.layer),
category: stringOrNull(value.category),
retryable: value.retryable === true,
userMessage: stringOrNull(value.userMessage),
route: stringOrNull(value.route),
toolName: stringOrNull(value.toolName)
};
}
function summarizeSessionReuse(value) {
if (!value || typeof value !== "object") return null;
return {