@sema-agent/core
Version:
Stateless, task-oriented AI agent core
215 lines (214 loc) • 13.6 kB
TypeScript
/**
* The provider dialect table — what each provider endpoint MEANS by the way it fails, declared once per
* endpoint and read by every seat that reports a provider failure.
*
* KEY. An endpoint is named by (wire family, host suffix + path): the same brand can serve two endpoints whose
* failures read in opposite ways (Moonshot's open platform states an exhausted balance as a 429, its coding
* subscription states a spent usage window as a 403 and keeps 429 for momentary overload), and the same word
* can mean opposite things on two brands (`insufficient_quota` is a per-minute token limit on DashScope and an
* empty balance on OpenAI). So the table is keyed by where the request went, never by vendor name or by word.
* A request whose endpoint matches no key reads only the GENERIC rows: the statements whose meaning does not
* depend on who said them.
*
* COLUMNS. A dialect is a record of named columns about one endpoint. `failures` is the failure-classification
* column: rows that turn a status, the envelope's word (`error.code`, else `error.type` — the same word
* `apiErrorCode` carries) and the envelope's sentence (`error.message`) into one {@link ProviderCondition}. The
* context-limit refusal is a flag on the same rows (`inputTooLong`), so "this 400 is the input being too long"
* and "this 400 is something else the provider named" are read off one row and cannot disagree.
*
* ONE VERDICT, TWO SEATS. {@link judgeHttpFailure} answers once per failed response; the retry decision, the
* retry frame (`BrainStatus.providerCondition`) and the terminal (`BrainError` → `AssistantMessage` →
* `terminal.detail`) all read that answer. An in-band error frame (a provider error delivered inside a 200
* stream) is judged by {@link judgeInBandError} over the same rows.
*
* DEGRADATION DIRECTION. Every row whose evidence is prose is fragile (a provider rewords and the row stops
* matching). A row that stops matching falls to the endpoint's own status row, then to the generic rows, then
* to the status alone — which in every case is the retrying reading, never the stopping one: a missed quota
* costs a backoff ladder, a false quota costs the user a run.
*/
import type { Model } from "../internal/llm.js";
import type { AssertAllKeysHandled } from "../core/ask-origin.js";
import { BrainError, type FailedBody } from "./errors.js";
/**
* What the provider said about the account or its own capacity when it failed a request — the one closed set
* both the retry frame and the terminal report:
* - `quota_exhausted` — the account cannot be served until someone acts or a window resets (an empty balance,
* an overdue bill, a spent usage window, a free tier used up). Not retried.
* - `throttled` — slow down: a request / token rate limit that clears on its own. Retried (backoff, the
* provider's `Retry-After` as the floor).
* - `overloaded` — the provider's own capacity, not the account's. Retried.
* - `stop_other` — a refusal that resending cannot change (credentials, model access, the request itself, its
* content, the plan's tier). Not retried. A context-limit refusal is one of these and is ALSO typed
* `input_too_long` on its own seat.
* - `unknown` — the endpoint said nothing this table can read. Retried exactly as the status alone would be.
*/
export declare const PROVIDER_CONDITIONS: readonly ["quota_exhausted", "throttled", "overloaded", "stop_other", "unknown"];
export type ProviderCondition = (typeof PROVIDER_CONDITIONS)[number];
/**
* Which evidence the condition rests on:
* - `unified_header` — a subscription quota header (`anthropic-ratelimit-unified-representative-claim` /
* `-overage-status` / `-overage-disabled-reason`) on a 429.
* - `provider_code` — a row keyed on the envelope's word (`error.code`, else `error.type`) or its sentence
* (`error.message`) matched.
* - `status` — no row read the envelope (none arrived, it did not end, or no row names its word or sentence): the
* default for what arrived — the endpoint's row for the status, else the generic row, else the status alone; an
* in-band error frame, which states no status, defaults to `unknown`.
*/
export declare const PROVIDER_CONDITION_SOURCES: readonly ["unified_header", "provider_code", "status"];
export type ProviderConditionSource = (typeof PROVIDER_CONDITION_SOURCES)[number];
/**
* What each condition means to this engine — the one place retryability lives:
* - `retryable` — the connect ladder re-sends a response judged so (the provider's explicit `x-should-retry`
* verdict still outranks it, in both directions);
* - `inBandCode` — the brain error code an in-band error frame carries (it has no status to classify by).
*/
export declare const PROVIDER_CONDITION_TRAITS: {
readonly quota_exhausted: {
readonly retryable: false;
readonly inBandCode: "rate_limit";
};
readonly throttled: {
readonly retryable: true;
readonly inBandCode: "rate_limit";
};
readonly overloaded: {
readonly retryable: true;
readonly inBandCode: "server";
};
readonly stop_other: {
readonly retryable: false;
readonly inBandCode: "invalid_request";
};
readonly unknown: {
readonly retryable: true;
readonly inBandCode: "server";
};
};
/** Compile-time fence: every condition has a traits row. */
export type ConditionTraitsCoverEveryCondition = AssertAllKeysHandled<Exclude<ProviderCondition, keyof typeof PROVIDER_CONDITION_TRAITS>>;
/** Membership read for a value that crossed a seat (a brand-read error member, a host-minted message). */
export declare function isProviderCondition(v: unknown): v is ProviderCondition;
export declare function isProviderConditionSource(v: unknown): v is ProviderConditionSource;
/**
* The context-limit refusal FAMILY, anchored at the START of the provider's diagnostic (`error.message`). The
* two error directions cost asymmetrically, and the asymmetry is what sets the width: a FALSE POSITIVE (a bare
* "Input is too long" prefix that is really field-length validation) costs one bounded recovery attempt on a
* request that already failed — the retry fails identically and the original error surfaces, self-limiting. A
* FALSE NEGATIVE (an unlisted GENUINE refusal read as an authoritative negative) permanently suppresses
* recovery for that turn: the task dies where it would have recovered. So within a PARSED provider envelope the
* whole conservative family classifies when it matches AT POSITION 0 of the diagnostic — "a provider stating
* this error states it first"; a reflection arrives behind a preamble ("validation failed; offending prompt: …")
* and the position-0 anchor is what separates the two. A prefixed genuine refusal inside a parsed envelope
* therefore reads as ruled-out — the residual narrowing, accepted (prose-only bodies keep the historic prose
* reach through the "unknown" arm).
*
* Members, by provenance: the loop's historic prose patterns re-anchored at position 0; the llama.cpp openings
* ("the request exceeds the available context size", "request (N tokens) exceeds …"); Gemini / Vertex through
* an OpenAI-compatible gateway ("the input token count (N) exceeds the maximum number of tokens allowed (M)");
* llama.cpp-family /v1 servers ("requested tokens (N) exceed context window of M"); two conservative gateway
* paraphrases ("your input exceeds the context window …", "the number of tokens in the request exceeds …");
* DashScope ("Range of input length should be [1, N]"); Moonshot's open platform ("Input token length too
* long"); Moonshot's coding endpoint ("Invalid request: Your request exceeded model token limit: N …" and
* "total message size N exceeds limit M"); Z.AI ("Prompt too long"). An unlisted subject-first provider spelling
* is a member to ADD; the anchor stays. A provider that names this refusal with a STRUCTURAL code says so on its
* own row instead (`context_length_exceeded` below, GLM's `1261`), and a code row outranks every sentence.
*/
export declare const INPUT_TOO_LONG_ANCHORED_RE: RegExp;
/** True when the (provider-authored) diagnostic OPENS with the context-limit refusal family. */
export declare function startsWithInputTooLongAnchor(diagnostic: string): boolean;
/**
* Three-state context-limit verdict: `"yes"` — the provider stated it, on a row that says so; `"no"` — the
* status cannot carry it, or the provider's envelope parsed and states something else; `"unknown"` — a status
* that can carry it whose body established no provenance (a text/plain gateway, or a body that did not end — the
* terminal marks the latter `errorBodyIncomplete`, so no seat reads its prose either).
*/
export type InputTooLongVerdict = "yes" | "no" | "unknown";
/** One row of the failure column. A row with no `status` applies to every status. */
export interface FailureRow {
readonly status?: readonly number[];
/** The envelope's word (`error.code` when a string, else `error.type`), compared exactly. */
readonly word?: readonly string[];
/** Matched against the envelope's `error.message`. Anchored unless the row's evidence says otherwise. */
readonly sentence?: RegExp;
readonly condition: ProviderCondition;
/** This row's statement IS the context-limit refusal. */
readonly inputTooLong?: true;
}
/** Where a dialect applies: the wire families it was evidenced for, its host suffixes, an optional path prefix. */
export interface DialectKey {
readonly wires: readonly Model["api"][];
/** A request host equal to one of these, or ending in `.` + one. */
readonly hosts: readonly string[];
/** A path prefix, matched at a segment boundary. Absent = every path on the host. */
readonly path?: string;
}
/** One endpoint's declared dialect: its key and its columns. */
export interface ProviderDialect {
readonly name: string;
readonly key: DialectKey;
readonly failures: readonly FailureRow[];
}
/**
* The declared dialects. Each row's source is the provider's own error documentation as collected in the
* per-provider matrix (status, word, sentence → condition); a `sentence` row is the fragile kind and degrades
* as the module note says.
*/
export declare const PROVIDER_DIALECTS: readonly ProviderDialect[];
/**
* The rows every endpoint answers to beneath its own, and the only rows an unrecognized endpoint has. Each
* states something whose meaning no provider in the table overloads: Payment Required, OpenAI's spend codes,
* the structural context-limit code and the context-limit sentences, the transport's 413. The cross-provider
* word `insufficient_quota` is pinned to `unknown` here on purpose — on an endpoint nobody declared, it is as
* likely a token-rate limit as an empty balance.
*/
export declare const GENERIC_FAILURES: readonly FailureRow[];
/**
* The TRANSIENT statuses — the server family and the two transient 4xx (408 request timeout, 409 lock timeout;
* CC's own retry predicate treats them like 5xx). Two readers, one fact: unread, such a status defaults to
* `unknown` (retried), and no row turns one into a stop (pinned), so the connect ladder decides them without
* reading the body (a stalled error body must not cost each retry the body's read bound); the terminal still
* reads and judges it.
*/
export declare function isTransientStatus(status: number): boolean;
/** The endpoint a request went to, or `undefined` when no dialect declares it. */
export declare function resolveProviderDialect(url: string, wire: Model["api"]): ProviderDialect | undefined;
/** One failure's reading off the table. */
export interface ProviderVerdict {
readonly condition: ProviderCondition;
readonly source: ProviderConditionSource;
/** When the condition is `quota_exhausted` and the provider stated when it lifts: wall-clock epoch ms. */
readonly resetAtMs?: number;
readonly inputTooLong: InputTooLongVerdict;
}
type HeaderBag = {
get?: (name: string) => string | null;
} | undefined | null;
/**
* Judge one failed HTTP response. `body` is the error body as read (`endedNormally` false = cut by a bound or
* torn): only a body that ENDED may steer, so a cut one is read as stating nothing — the endpoint's status row
* and the generic rows still apply. Absent `body` = not read (a status whose verdict no body can change).
*/
export declare function judgeHttpFailure(args: {
dialect: ProviderDialect | undefined;
status: number;
headers?: HeaderBag;
body?: FailedBody;
}): ProviderVerdict;
/** Judge an in-band error frame's envelope object (a provider error delivered inside a 200 stream). */
export declare function judgeInBandError(dialect: ProviderDialect | undefined, envelope: {
message?: unknown;
code?: unknown;
type?: unknown;
}): ProviderVerdict;
/**
* The failure an in-band error frame states, as the brain error the stream's terminal carries: the code its
* condition reads as ({@link PROVIDER_CONDITION_TRAITS}), the provider's word and sentence in the message, the word
* on the echo seat, the verdict, and the context-limit reading of the same row. A frame the provider sent on its
* own stream is a provider failure by construction (`apiError`); it states no status.
*/
export declare function inBandProviderError(dialect: ProviderDialect | undefined, envelope: {
message?: unknown;
code?: unknown;
type?: unknown;
}, label: string): BrainError;
export {};