export enum ErrorCode { // Provider errors PROVIDER_ERROR = "PROVIDER_ERROR", // Tool errors TOOL_ERROR = "TOOL_ERROR", // Permission errors PERMISSION_DENIED = "PERMISSION_DENIED", // Config errors CONFIG_ERROR = "CONFIG_ERROR", // Daemon errors DAEMON_ERROR = "DAEMON_ERROR", // Platform-specific errors (clipboard, unsupported OS features) PLATFORM_ERROR = "PLATFORM_ERROR", // WASM integrity check failures INTEGRITY_ERROR = "INTEGRITY_ERROR", // Internal/unexpected errors INTERNAL_ERROR = "INTERNAL_ERROR", } // ── Root ────────────────────────────────────────────────────────────────────── /** Root base class for all named Vellum errors. */ export class VellumError extends Error { constructor(message: string, options?: ErrorOptions) { super(message, options); this.name = "VellumError"; } } // ── Backend errors ──────────────────────────────────────────────────────────── /** * Errors originating from infrastructure or external service calls. * Catch this when you want to handle any backend failure uniformly. */ export class BackendError extends VellumError { constructor(message: string, options?: ErrorOptions) { super(message, options); this.name = "BackendError"; } } /** * The embedding or vector-search backend is not configured or not reachable. * Thrown before attempting an embedding operation so callers can skip or defer. */ export class BackendUnavailableError extends BackendError { constructor(reason: string) { super(reason); this.name = "BackendUnavailableError"; } } /** * A request or token-budget quota was exceeded. * Thrown by the provider rate-limiter and by domain-specific clients (e.g. DoorDash). */ export class RateLimitError extends BackendError { constructor(message: string) { super(message); this.name = "RateLimitError"; } } // ── User errors ─────────────────────────────────────────────────────────────── /** * Errors caused by user input, policy violations, or user-facing constraints. * Catch this when you want to present an actionable message to the user. */ export class UserError extends VellumError { constructor(message: string, options?: ErrorOptions) { super(message, options); this.name = "UserError"; } } // ── AssistantError subtree ──────────────────────────────────────────────────── /** * Base class for errors originating from assistant logic (providers, tools, * permissions, config). Extends VellumError and carries a structured ErrorCode. */ export class AssistantError extends VellumError { constructor( message: string, public readonly code: ErrorCode, options?: { cause?: unknown }, ) { super(message, options); this.name = "AssistantError"; } } /** * Semantic classification of a provider failure, stamped by the provider layer * at the throw site (from status + body signals) so downstream consumers * (`daemon/conversation-error.ts`, `providers/retry.ts`) can switch on intent * instead of re-deriving it from status codes and message regex. Co-located * with `ProviderError` — a leaf module — for the same reason as `abortReason`: * no import cost. `unknown` (and an absent `reason`) defers to the existing * status/regex fallback. */ export type ProviderErrorReason = | "invalid_credentials" | "model_restricted" | "model_not_found" | "insufficient_credits" | "daily_limit_reached" | "rate_limited" | "overloaded" | "context_overflow" | "vision_unsupported" | "bad_request" | "server_error" | "network_error" | "unknown"; export type ProviderCredentialSource = | "byok" | "no-auth" | "oauth-subscription" | "vellum-managed"; export interface ProviderRouteAttribution { connectionName?: string; profileName?: string; isManagedRoute?: boolean; /** * Credential surface used for this request. Finer than `isManagedRoute`: * it separates personal provider keys, keyless endpoints, and subscription * logins from each other so error copy can name what the user must fix. */ credentialSource?: ProviderCredentialSource; } export class ProviderError extends AssistantError { /** Delay (in ms) suggested by the server's Retry-After header, if present. */ public readonly retryAfterMs?: number; /** Upstream provider error metadata, parsed from the raw non-2xx body. */ public readonly apiErrorCode?: string; public readonly apiErrorType?: string; public readonly apiErrorParam?: string; public readonly requestId?: string; /** * Verbatim upstream non-2xx body (possibly truncated). Persisted so the * inspector's Raw tab can show the actual provider error payload, not just * the extracted fields. */ public readonly rawBody?: string; /** * Tagged daemon-owned abort reason carried over from the AbortSignal that * triggered this error. Untyped here to avoid a daemon→util import cycle; * `AbortReason` from `util/abort-reasons.ts` is the only producer and * `isAbortReason` is the canonical type guard for consumers. */ public readonly abortReason?: unknown; /** Semantic failure classification stamped at the throw site. */ public readonly reason?: ProviderErrorReason; /** Transport route selected for the failed provider call. */ public routeAttribution?: ProviderRouteAttribution; constructor( message: string, public readonly provider: string, public readonly statusCode?: number, options?: { cause?: unknown; retryAfterMs?: number; abortReason?: unknown; apiErrorCode?: string; apiErrorType?: string; apiErrorParam?: string; requestId?: string; rawBody?: string; reason?: ProviderErrorReason; }, ) { super(message, ErrorCode.PROVIDER_ERROR, options); this.name = "ProviderError"; this.retryAfterMs = options?.retryAfterMs; this.apiErrorCode = options?.apiErrorCode; this.apiErrorType = options?.apiErrorType; this.apiErrorParam = options?.apiErrorParam; this.requestId = options?.requestId; this.rawBody = options?.rawBody; this.abortReason = options?.abortReason; this.reason = options?.reason; } attachRouteAttribution(attribution: ProviderRouteAttribution): void { this.routeAttribution = { ...attribution, ...this.routeAttribution, }; } } export class ToolError extends AssistantError { constructor( message: string, public readonly toolName: string, ) { super(message, ErrorCode.TOOL_ERROR); this.name = "ToolError"; } } export class PermissionDeniedError extends AssistantError { constructor( message: string, public readonly toolName: string, ) { super(message, ErrorCode.PERMISSION_DENIED); this.name = "PermissionDeniedError"; } } export class ConfigError extends AssistantError { constructor(message: string, options?: { cause?: unknown }) { super(message, ErrorCode.CONFIG_ERROR, options); this.name = "ConfigError"; } } export class ProviderNotConfiguredError extends ConfigError { /** * Optional name of the `provider_connections` row whose credential was * missing. Surfaced through `ConversationErrorEvent.connectionName` so * the macOS chat banner can render "API key required for connection * " instead of a generic message. */ public readonly connectionName?: string; /** * Optional name of the resolved profile in play when this error was * thrown. Forwarded to the wire `ConversationErrorEvent.profileName` * for the same banner-attribution purpose as `connectionName`. */ public readonly profileName?: string; constructor( public readonly requestedProvider: string, public readonly registeredProviders: string[], attribution?: { connectionName?: string; profileName?: string }, ) { super( `No providers available. Requested: "${requestedProvider}". Registered: ${registeredProviders.join(", ") || "none"}`, ); this.name = "ProviderNotConfiguredError"; this.connectionName = attribution?.connectionName; this.profileName = attribution?.profileName; } } export class DaemonError extends AssistantError { constructor(message: string, options?: { cause?: unknown }) { super(message, ErrorCode.DAEMON_ERROR, options); this.name = "DaemonError"; } } export class IntegrityError extends AssistantError { constructor(message: string, options?: { cause?: unknown }) { super(message, ErrorCode.INTEGRITY_ERROR, options); this.name = "IntegrityError"; } }