@sema-agent/core
Version:
Stateless, task-oriented AI agent core
179 lines (178 loc) • 6.72 kB
JavaScript
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { proxyTunnelRefusedSentence, readConnectFailure } from "../brain/errors.js";
export const MCP_FAILURE_KINDS = Object.freeze([
"connect_refused",
"connection_failed",
"connection_closed",
"http_status",
"not_mcp_response",
"spawn_failed",
"timeout",
"protocol",
"invalid_config",
"unknown",
]);
export const MCP_DELIVERY_VERDICTS = Object.freeze(["yes", "no", "unknown"]);
export const DELIVERED_BY_KIND = {
connect_refused: "no",
connection_failed: "unknown",
connection_closed: "unknown",
http_status: "unknown",
not_mcp_response: "unknown",
spawn_failed: "no",
timeout: "unknown",
protocol: "yes",
invalid_config: "no",
unknown: "unknown",
};
export const MCP_LIVENESS_STATES = Object.freeze(["reachable", "unreachable", "unknown"]);
export const LIVENESS_BY_FAILURE_KIND = {
connect_refused: "unreachable",
connection_failed: "unreachable",
connection_closed: "unreachable",
http_status: "unknown",
not_mcp_response: "unknown",
spawn_failed: "unreachable",
timeout: "unreachable",
protocol: "reachable",
invalid_config: undefined,
unknown: "unknown",
};
export function livenessOf(outcome, observedAt) {
if (outcome === "completed")
return { state: "reachable", observedAt };
const state = LIVENESS_BY_FAILURE_KIND[outcome];
if (state === undefined)
return undefined;
return state === "unreachable" ? { state, observedAt, errorCode: outcome } : { state, observedAt };
}
const CONNECT_KEEPS_THE_TABLE_VERDICT = new Set(["protocol", "http_status"]);
const NETWORK_CODES_NEVER_DELIVERED = new Set([
"ECONNREFUSED",
"ENOTFOUND",
"EAI_AGAIN",
"EHOSTUNREACH",
"ENETUNREACH",
"UND_ERR_CONNECT_TIMEOUT",
]);
export function networkErrorCode(err, depth = 0) {
if (depth > 5 || !(err instanceof Error))
return undefined;
const code = err.code;
if (typeof code === "string" && /^(?:E[A-Z_]+|UND_ERR_[A-Z_]+)$/.test(code))
return code;
if (err instanceof AggregateError) {
for (const inner of err.errors) {
const found = networkErrorCode(inner, depth + 1);
if (found !== undefined)
return found;
}
}
return networkErrorCode(err.cause, depth + 1);
}
const isNodeErrCode = (code) => code.startsWith("ERR_");
const isDeclarationFault = (code) => code === "ERR_INVALID_URL" || (typeof code === "string" && code.startsWith("config."));
export const FAILURE_CLASSIFIER = [
{
name: "sdk connection closed",
when: (err) => err instanceof McpError && err.code === ErrorCode.ConnectionClosed,
reads: () => ({ kind: "connection_closed" }),
},
{
name: "sdk request timeout",
when: (err) => err instanceof McpError && err.code === ErrorCode.RequestTimeout,
reads: () => ({ kind: "timeout" }),
},
{
name: "server answered with a protocol error",
when: (err) => err instanceof McpError,
reads: () => ({ kind: "protocol" }),
},
{
name: "http endpoint answered a status",
when: (err) => err instanceof StreamableHTTPError && typeof err.code === "number" && err.code > 0,
reads: (err) => ({ kind: "http_status", httpStatus: err.code }),
},
{
name: "http endpoint answered a non-mcp payload",
when: (err) => err instanceof StreamableHTTPError,
reads: () => ({ kind: "not_mcp_response" }),
},
{
name: "malformed declaration",
when: (err, at) => at.phase === "connect" && err instanceof Error && isDeclarationFault(err.code),
reads: () => ({ kind: "invalid_config" }),
},
{
name: "proxy refused the tunnel",
when: (err) => readConnectFailure(err).proxyRefusal !== undefined,
reads: () => ({ kind: "connect_refused" }),
},
{
name: "node error code, not a network errno",
when: (err) => {
const code = networkErrorCode(err);
return code !== undefined && isNodeErrCode(code);
},
reads: () => ({ kind: "unknown" }),
},
{
name: "stdio spawn failed",
when: (err, at) => at.phase === "connect" && at.transport === "stdio" && networkErrorCode(err) !== undefined,
reads: () => ({ kind: "spawn_failed" }),
},
{
name: "connect-phase errno",
when: (err) => {
const code = networkErrorCode(err);
return code !== undefined && NETWORK_CODES_NEVER_DELIVERED.has(code);
},
reads: () => ({ kind: "connect_refused" }),
},
{
name: "network errno past the connect phase",
when: (err) => networkErrorCode(err) !== undefined,
reads: () => ({ kind: "connection_failed" }),
},
{
name: "no structure",
when: () => true,
reads: () => ({ kind: "unknown" }),
},
];
export function classifyMcpFailure(err, at) {
for (const rule of FAILURE_CLASSIFIER) {
if (!rule.when(err, at))
continue;
const read = rule.reads(err);
const dialled = at.phase === "connect" && !CONNECT_KEEPS_THE_TABLE_VERDICT.has(read.kind);
return { ...read, delivered: dialled ? "no" : DELIVERED_BY_KIND[read.kind] };
}
return { kind: "unknown", delivered: "unknown" };
}
export function describeHttpTransportFailure(err) {
const f = classifyMcpFailure(err, { phase: "request" });
switch (f.kind) {
case "http_status":
return { condition: `its HTTP endpoint answered ${f.httpStatus} instead of an MCP response`, delivered: f.delivered, httpStatus: f.httpStatus };
case "not_mcp_response":
return { condition: "its HTTP endpoint answered something that is not an MCP response", delivered: f.delivered };
case "connect_refused": {
const proxy = readConnectFailure(err).proxyRefusal;
return {
condition: `its HTTP endpoint could not be reached (${proxy !== undefined ? proxyTunnelRefusedSentence(proxy.status) : networkErrorCode(err)})`,
delivered: f.delivered,
};
}
case "connection_failed":
return { condition: `the connection to its HTTP endpoint failed (${networkErrorCode(err)})`, delivered: f.delivered };
case "connection_closed":
case "spawn_failed":
case "timeout":
case "protocol":
case "invalid_config":
case "unknown":
return undefined;
}
}