@sema-agent/core
Version:
Stateless, task-oriented AI agent core
216 lines (215 loc) • 14.2 kB
TypeScript
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;