UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

215 lines (214 loc) • 13.6 kB
/** * 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 {};