UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

167 lines (166 loc) • 6.15 kB
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.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_"); 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 && err.code === "ERR_INVALID_URL", reads: () => ({ kind: "invalid_config" }), }, { 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": return { condition: `its HTTP endpoint could not be reached (${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; } }