UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

222 lines (221 loc) • 8.39 kB
import { isErrorLike, readMember, thrownText } from "../core/thrown-value.js"; import { emitBrainTelemetry } from "./status-sink.js"; import { isProviderEchoShape } from "./provider-echo-shape.js"; export const ZERO_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }; const BRAIN_ERROR_CODES = ["auth", "rate_limit", "invalid_request", "server", "network", "http", "stream_torn", "refusal", "length_empty"]; export const ALL_BRAIN_ERROR_CODES = BRAIN_ERROR_CODES; export const IN_BAND_OUTPUT_CODES = new Set(["stream_torn", "refusal", "length_empty"]); export class BrainError extends Error { code; status; inputTooLong; inputTooLongRuledOut; apiError; providerCode; constructor(code, detail, status, opts) { super(`[${code}] ${detail}`); this.name = "BrainError"; this.code = code; this.status = status; if (opts?.inputTooLong === true) this.inputTooLong = true; else if (opts?.inputTooLongRuledOut === true) this.inputTooLongRuledOut = true; if (opts?.apiError === true) this.apiError = true; if (opts?.providerCode !== undefined) this.providerCode = opts.providerCode; } } export function providerErrorObject(body) { const trimmed = body.trim(); if (!trimmed.startsWith("{")) return undefined; let parsed; try { parsed = JSON.parse(trimmed); } catch { return undefined; } if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined; const err = parsed.error; if (typeof err !== "object" || err === null || Array.isArray(err)) return undefined; return err; } export function readProviderErrorCode(body) { const err = providerErrorObject(body); const word = typeof err?.code === "string" ? err.code : typeof err?.type === "string" ? err.type : undefined; const reading = providerEchoReading(word); if (reading === "carried") return word; if (reading === "refused") emitBrainTelemetry({ kind: "echo_dropped", field: "apiErrorCode" }); return undefined; } const REQUEST_ID_HEADERS = ["request-id", "x-request-id"]; export { isProviderEchoShape, PROVIDER_ECHO_SHAPE } from "./provider-echo-shape.js"; export function providerEchoReading(value) { if (typeof value !== "string" || value.length === 0) return "absent"; return isProviderEchoShape(value) ? "carried" : "refused"; } export function readProviderRequestId(res) { let refused = false; try { const get = res?.headers?.get; if (typeof get !== "function") return undefined; for (const name of REQUEST_ID_HEADERS) { const raw = get.call(res.headers, name); const reading = providerEchoReading(raw); if (reading === "carried") return raw; if (reading === "refused") refused = true; } } catch { } if (refused) emitBrainTelemetry({ kind: "echo_dropped", field: "requestId" }); return undefined; } export function namesTheFailure(status) { return status !== undefined && status >= 100 && !(status >= 200 && status < 300); } export function refusedTheRequest(status) { return status !== undefined && status >= 400 && status < 500; } export function classifyHttp(status) { if (status === 401 || status === 403) return "auth"; if (status === 429) return "rate_limit"; if (status === 400 || status === 422) return "invalid_request"; if (status === 408 || status === 409) return "server"; if (status >= 500) return "server"; return "http"; } const CODE_ALTERNATION = BRAIN_ERROR_CODES.join("|"); const CODE_RE = new RegExp(`^\\[(${CODE_ALTERNATION})\\]`); const CODE_PREFIX_RE = new RegExp(`^\\[(?:${CODE_ALTERNATION})\\]\\s*`); export function extractErrorCode(errorMessage) { const m = errorMessage ? CODE_RE.exec(errorMessage) : null; return m ? m[1] : undefined; } export function stripErrorCodePrefix(errorMessage) { return errorMessage.replace(CODE_PREFIX_RE, ""); } const STALE_CONNECTION_CODES = new Set([ "ECONNRESET", "EPIPE", "ConnectionClosed", "ETIMEDOUT", "ECONNABORTED", "ERR_SOCKET_CLOSED", "StreamSuspended", ]); function hasStaleConnectionCode(e) { let cur = e; for (let depth = 0; depth < 5 && cur !== undefined && cur !== null; depth++) { const code = readMember(cur, "code"); if (typeof code === "string" && STALE_CONNECTION_CODES.has(code)) return true; cur = isErrorLike(cur) ? readMember(cur, "cause") : undefined; } return false; } const DETERMINISTIC_CONNECT_CODES = new Set(["ECONNREFUSED", "ENOTFOUND"]); const CONNECT_GRAPH_NODE_BUDGET = 16; function walkConnectFailure(node, budget) { if (budget.left <= 0) return "indeterminate"; budget.left--; if (node === undefined || node === null) return "silent"; let verdict = "silent"; const code = readMember(node, "code"); if (typeof code === "string") { if (!DETERMINISTIC_CONNECT_CODES.has(code)) return "indeterminate"; verdict = "deterministic"; } if (!isErrorLike(node)) return verdict; const members = readMember(node, "errors"); const legCount = readMember(members, "length"); if (members !== undefined) { if (typeof legCount !== "number" || !Number.isInteger(legCount) || legCount <= 0) return "indeterminate"; for (let i = 0; i < legCount; i++) { if (walkConnectFailure(readMember(members, String(i)), budget) !== "deterministic") return "indeterminate"; } verdict = "deterministic"; } const cause = readMember(node, "cause"); if (cause !== undefined && cause !== null) { const below = walkConnectFailure(cause, budget); if (below === "indeterminate") return "indeterminate"; if (below === "deterministic") verdict = "deterministic"; } return verdict; } export function classifyConnectFailure(e) { return walkConnectFailure(e, { left: CONNECT_GRAPH_NODE_BUDGET }) === "deterministic" ? "deterministic" : "indeterminate"; } const FETCH_TRANSPORT_SHELLS = new Set(["fetch failed", "terminated"]); const REQUEST_ARGUMENT_CODES = new Set(["ERR_INVALID_URL"]); export function classifyFetchThrow(e) { if (readMember(e, "name") !== "TypeError") return "transport"; const message = readMember(e, "message"); if (typeof message === "string" && FETCH_TRANSPORT_SHELLS.has(message)) return "transport"; const cause = readMember(e, "cause"); if (cause === undefined || cause === null) return "request_unbuildable"; const code = readMember(cause, "code"); return typeof code === "string" && REQUEST_ARGUMENT_CODES.has(code) ? "request_unbuildable" : "transport"; } export function abortLikeName(err) { if (!isErrorLike(err)) return undefined; const name = readMember(err, "name"); return name === "AbortError" || name === "TimeoutError" ? name : undefined; } export function brainErrorCodeOf(e) { const code = readMember(e, "code"); return typeof code === "string" && BRAIN_ERROR_CODES.includes(code) ? code : undefined; } export function describeNetworkError(e) { const top = thrownText(e); const parts = []; let cur = isErrorLike(e) ? readMember(e, "cause") : undefined; for (let depth = 0; depth < 4 && cur !== undefined && cur !== null; depth++) { const rawCode = readMember(cur, "code"); const code = typeof rawCode === "string" ? rawCode : undefined; const msg = thrownText(cur); const piece = code !== undefined && !msg.includes(code) ? `${code}: ${msg}` : msg; if (piece.length > 0 && piece !== top && !parts.includes(piece)) parts.push(piece); cur = isErrorLike(cur) ? readMember(cur, "cause") : undefined; } const body = parts.length > 0 ? `${top} (${parts.join(" ← ")})` : top; return hasStaleConnectionCode(e) ? `${body} — possibly a stale connection; a fresh one may succeed` : body; }