UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

216 lines (215 loc) • 14.2 kB
import type { AssertAllKeysHandled } from "./ask-origin.js"; /** * The closed set of failure classes ({@link McpFailure.kind}): * · `connect_refused` — a connect-phase errno (ECONNREFUSED / ENOTFOUND / EAI_AGAIN / EHOSTUNREACH / * ENETUNREACH / UND_ERR_CONNECT_TIMEOUT): the request provably never left; * · `connection_failed` — any other errno / undici code on the cause chain (a reset or a timeout * mid-exchange): the request may have run; * · `connection_closed` — the SDK reported the transport closed (`McpError` `ConnectionClosed`): in * flight the fate is unknowable, and for a server already known dead the * request was never attempted — the two are told apart by `delivered`; * · `http_status` — the HTTP endpoint answered with a status instead of an MCP response * (`httpStatus` carries the number: 401/403 = re-authorize, 5xx = down); * · `not_mcp_response` — the HTTP endpoint answered something that is not an MCP payload and no status; * · `spawn_failed` — a stdio server process could not be started (a missing/unexecutable command); * · `timeout` — the request timed out (the SDK's per-call timer, the total ceiling, or the * idle watchdog — `details` names which); * · `protocol` — the server ANSWERED with an MCP/JSON-RPC error (a version mismatch, a rejected * capability, an ordinary `-32603`): the exchange happened; * · `invalid_config` — the declaration itself could not be dialed (a malformed `url`): a fault to * fix in the spec, never an outage to wait out; * · `unknown` — the thrown value carried no structure this table reads. */ export declare const MCP_FAILURE_KINDS: readonly ["connect_refused", "connection_failed", "connection_closed", "http_status", "not_mcp_response", "spawn_failed", "timeout", "protocol", "invalid_config", "unknown"]; export type McpFailureKind = (typeof MCP_FAILURE_KINDS)[number]; /** The closed set of delivery verdicts ({@link McpFailure.delivered}). */ export declare const MCP_DELIVERY_VERDICTS: readonly ["yes", "no", "unknown"]; export type McpDelivered = (typeof MCP_DELIVERY_VERDICTS)[number]; /** * The delivery verdict each KIND carries when the failure happened on an established connection * (`phase: "request"`) — the disposition table over {@link MCP_FAILURE_KINDS}. The classifier's rows * read the thrown value's STRUCTURE and mint a class; what that class means for delivery is decided * here, once, so no two rows can disagree about the same word and a new kind cannot arrive without * saying what it means for delivery (the fence below reds `tsc` with the missing word spelled out). */ export declare const DELIVERED_BY_KIND: { readonly connect_refused: "no"; readonly connection_failed: "unknown"; readonly connection_closed: "unknown"; readonly http_status: "unknown"; readonly not_mcp_response: "unknown"; readonly spawn_failed: "no"; readonly timeout: "unknown"; readonly protocol: "yes"; readonly invalid_config: "no"; readonly unknown: "unknown"; }; /** The fence over the delivery table: `never` while every kind has a verdict. */ export type DeliveryTableCoversEveryMcpFailureKind = AssertAllKeysHandled<Exclude<McpFailureKind, keyof typeof DELIVERED_BY_KIND>>; /** * The closed set of {@link McpServerLiveness.state} — the engine's answer to ONE question, "can this * client complete an exchange with this server": * · `reachable` — an exchange COMPLETED (the dial handshaked and listed, a re-dial succeeded, or the * server itself answered — an MCP-level rejection is an answer, so it proves reach * even though it failed); * · `unreachable` — an exchange was attempted and lost at the transport or the clock (refused, reset, * closed, unspawnable, timed out); * · `unknown` — an exchange was attempted and what came back does not answer the question (a status * a gateway in front of the server may have written, a non-MCP payload, a thrown value * with no structure this table reads). * ABSENCE is the fourth reading and is NOT one of these words: no observation exists at all (the leg * never reached this server, or the declaration could not be dialed). A consumer must not fold an absent * record into either `unknown` or a health verdict — absent means indeterminate, and `unknown` means * "we looked and could not tell". */ export declare const MCP_LIVENESS_STATES: readonly ["reachable", "unreachable", "unknown"]; export type McpLivenessState = (typeof MCP_LIVENESS_STATES)[number]; /** * One liveness OBSERVATION about one server (see {@link MCP_LIVENESS_STATES} for the words). It answers * what the engine last LEARNED, at `observedAt` (epoch ms) — never what is true now, and never a poll: * the engine makes no probe of its own, so every record here is a by-product of work the task already * did (the dial, a re-dial, a transport close). * * `errorCode` rides `unreachable` ONLY, and names the failure class that decided it (a `spawn_failed` is * a missing binary, a `connect_refused` a closed port). It is deliberately absent on `unknown`: the whole * point of that word is that the failure did NOT decide reachability, so carrying its class there would * invite a consumer to key a reach action off evidence that says nothing about reach. The connect-time * class is still reported in its own right beside this record (the status / manifest row's `errorCode`). */ export interface McpServerLiveness { state: McpLivenessState; observedAt: number; errorCode?: McpFailureKind; } /** * What each failure CLASS says about reachability — the second disposition table over * {@link MCP_FAILURE_KINDS}, beside {@link DELIVERED_BY_KIND}, and deliberately NOT derived from it: * delivery asks "did the caller's request run", reachability asks "did an exchange complete", and the two * part company on both edges — a `timeout` leaves delivery `unknown` while the exchange provably did not * complete, and a `protocol` rejection is a failure the caller must handle while being the strongest * proof of reach there is. * * `invalid_config` maps to `undefined` — the one class that is NOT an observation: a declaration that * could not be dialed means no transport was ever attempted, so the honest record is no record (see the * absence reading in {@link MCP_LIVENESS_STATES}), not a third-hand `unknown`. */ export declare const LIVENESS_BY_FAILURE_KIND: { readonly connect_refused: "unreachable"; readonly connection_failed: "unreachable"; readonly connection_closed: "unreachable"; readonly http_status: "unknown"; readonly not_mcp_response: "unknown"; readonly spawn_failed: "unreachable"; readonly timeout: "unreachable"; readonly protocol: "reachable"; readonly invalid_config: undefined; readonly unknown: "unknown"; }; /** The fence over the liveness table: `never` while every kind has a verdict. */ export type LivenessTableCoversEveryMcpFailureKind = AssertAllKeysHandled<Exclude<McpFailureKind, keyof typeof LIVENESS_BY_FAILURE_KIND>>; /** * What ONE exchange with a server tells the liveness record: the CLASS of the failure it ended in, or * `"completed"` when it finished. Every round trip the engine makes — a dial, a tool call, a resource * read, a listing, a refresh — reports exactly this, so the engine needs no second vocabulary and no * per-site judgment about which failures "count". */ export type McpExchangeOutcome = McpFailureKind | "completed"; /** The liveness observation an exchange supports, or `undefined` when the outcome is not an observation * at all (see {@link LIVENESS_BY_FAILURE_KIND}: a declaration that could never be dialed). THE single * mint of the record — no two sites can disagree about the word, about which words may carry * `errorCode`, or about what a completed exchange proves. */ export declare function livenessOf(outcome: McpExchangeOutcome, observedAt: number): McpServerLiveness | undefined; /** Where the failure was judged: at the dial (connect + initial listing), where an errno on a stdio * transport is the child process failing to spawn, or on a request over an established connection. */ export type McpFailureSite = { phase: "connect"; transport: "stdio" | "http" | undefined; } | { phase: "request"; }; /** * The one MCP failure record. Both fields are always present; `httpStatus` rides only with * `kind === "http_status"`. */ export interface McpFailure { /** * @contract mcp.failure.kind — the failure's CLASS, one of {@link MCP_FAILURE_KINDS}, decided by * {@link classifyMcpFailure} from the thrown value's structure alone (never from its message). It is * the SAME word on every face: the per-server status's and the wiring manifest's `errorCode`, the * thrown request error's `details.code`, and the `tool_end` frame's `errorCode`. Closed: a consumer * switching on it is complete; a new class is a new word for consumers, never a re-meaning of one. */ kind: McpFailureKind; /** * @contract mcp.failure.delivered — whether the request REACHED the server, as a first-class fact * beside the class: `"yes"` = the server answered (a `protocol` rejection proves the exchange * happened); `"no"` = provably never sent (a connect-phase errno, a spawn failure, a malformed * declaration, a server already known dead — safe to retry or abandon, nothing executed); `"unknown"` * = the client stopped waiting or lost the pipe mid-exchange (a timeout, an in-flight close, a reset, * an HTTP status from a gateway in front of the server) — the request MAY have executed, so a * write-capable tool's side effects must be verified before a retry. Two failures with the same * `kind` can differ here (`connection_closed` in flight vs. against a dead server), which is why the * verdict is its own field and not folded into the class. It is a function of the class and the SITE, * never of the row that matched: {@link DELIVERED_BY_KIND} says what a class means on an established * connection, and at the dial the verdict is `"no"` for every class but the two minted from an answer * ({@link CONNECT_KEEPS_THE_TABLE_VERDICT}) — a connect failure is a server whose tools never mounted, * so no TOOL CALL of the caller's was dispatched, let alone executed. Read * {@link CONNECT_KEEPS_THE_TABLE_VERDICT} for what a dial's `"no"` does not promise (an elicitation * answered during the dial has already crossed the connection) and for the facts that would let the * rule be narrower. */ delivered: McpDelivered; /** The HTTP status the endpoint answered with; present iff `kind === "http_status"`. */ httpStatus?: number; } /** * The first errno-shaped / undici-shaped `code` on an error's cause chain (`E[A-Z_]+`, `UND_ERR_*`, * Node's `ERR_*`). `fetch` reports every network failure as a bare `TypeError: fetch failed` with the * real cause nested underneath (and an AggregateError when a host resolved to several addresses), so * the code is never on the thrown error itself. The E-prefixed alternative allows `_`: Node's * DNS-temporary-failure errno is literally `EAI_AGAIN`. */ export declare function networkErrorCode(err: unknown, depth?: number): string | undefined; /** What a row reads OFF the thrown value: the failure's class, and the status the `http_status` class * carries. A row does NOT write the delivery verdict — that follows from the kind and the SITE, in * one place ({@link DELIVERED_BY_KIND} + {@link classifyMcpFailure}), so a row cannot spell it wrong. */ export type McpFailureClass = { kind: McpFailureKind; httpStatus?: number; }; /** One row of the classifier: `when` reads structure only; `reads` names the class it saw. */ export interface McpFailureRule { /** The row's name, for the discrimination pins (one pin per row) and for a reader of a verdict. */ readonly name: string; readonly when: (err: unknown, at: McpFailureSite) => boolean; readonly reads: (err: unknown) => McpFailureClass; } /** * The ORDERED classifier. First match wins; the final row matches everything. The order is part of * the contract — the SDK's typed errors go first because they are an answer FROM the exchange (an * McpError is never a transport failure, whatever its text says), then the HTTP transport's typed * error, then the cause-chain codes from most specific (a configuration fault, a spawn) to least * (a connect-phase errno, any other network code). */ export declare const FAILURE_CLASSIFIER: ReadonlyArray<McpFailureRule>; /** * Classify one MCP failure into {@link McpFailure}: the CLASS comes from the first matching row of * {@link FAILURE_CLASSIFIER} (structure only), the VERDICT from {@link DELIVERED_BY_KIND} and the site * — one predicate for the dial ({@link CONNECT_KEEPS_THE_TABLE_VERDICT}), never a per-row spelling. * Total: every input maps to a record. */ export declare function classifyMcpFailure(err: unknown, at: McpFailureSite): McpFailure; /** * What a transport-level HTTP failure can be said to be, in words for a model-facing line: the * condition to name, and the delivery verdict. `undefined` for every failure that is NOT of the HTTP * transport family (an McpError is an answer FROM the server; a bare error with no structure is * `unknown` and gets no transport story). Shared with the A2A client leg, which faces the SAME `fetch` * layer and owes the same outcome contract — one classifier, so the two protocols cannot drift on what * "the request never reached them" means. */ export interface HttpTransportFailure { condition: string; delivered: McpDelivered; httpStatus?: number; } export declare function describeHttpTransportFailure(err: unknown): HttpTransportFailure | undefined;