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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user