/** * Closed enum of executor failure modes. The set is the *only* surface the * framework exposes about *why* a panel failed — it never widens to include * driver error strings, ZodError details, or any value derived from query * input or output. * * Adding a new category is a security-relevant change: the new value becomes * part of the bounded surface the LLM may see (via downstream consumers that * route categories into prompts), so each addition needs review against * Invariant 2 (no externally-sourced values in model-visible output). */ export type CarteExecutionErrorCategory = | "unknown_query" | "invalid_params" | "query_failed" | "returns_mismatch" | "context_unauthorized"; /** * Sentinel `panelIndex` for `CarteExecutionError`s thrown during context * derivation — before any panel is associated with a query. The context * factory runs once per request, prior to plan execution; using `-1` * preserves the public `{ panelIndex, queryId, ... }` shape of * `CarteExecutionErrorPublic` (no schema change for downstream consumers) * while remaining recognisable as "not a real panel." * * Integration packages (e.g. `@usecarte/better-auth`) MUST use this constant * rather than the literal `-1` so contributors don't read it as a real * panel index. */ export const CONTEXT_PHASE_PANEL_INDEX = -1; /** * Sentinel `queryId` paired with `CONTEXT_PHASE_PANEL_INDEX`. Same rationale: * keeps the projection shape stable; signals "this failure happened before * any query was selected." Integration packages MUST use this constant * rather than the literal string `""` so contributors don't read * it as a real query id. */ export const CONTEXT_PHASE_QUERY_ID = ""; /** * Public, model-safe projection of a `CarteExecutionError`. This is the only * shape `@usecarte/*` will ever serialize to a model-visible surface (HTTP * response body, retry prompt, etc.) — `cause` and the underlying `Error.message` * are deliberately excluded. * * Consumers who want richer diagnostics should attach a `logger` to * `executePlan` and surface the full error server-side, never to the model. */ export interface CarteExecutionErrorPublic { category: CarteExecutionErrorCategory; panelIndex: number; queryId: string; correlationId: string; } /** * Sink for server-side observability. Receives the underlying `cause` (driver * error / ZodError / etc.) along with the framework-controlled metadata, so * consumers can write the full error to Sentry / Datadog / structured logs. * * The default is `console.error`. Wire a real sink in production; the model * never sees what the logger receives. */ export type CarteExecutionLogger = (event: { category: CarteExecutionErrorCategory; panelIndex: number; queryId: string; correlationId: string; cause: unknown; }) => void; /** * Default logger — prefixes the correlation ID so server-side log searches * line up with model-visible error references. */ export const defaultExecutionLogger: CarteExecutionLogger = (event) => { // eslint-disable-next-line no-console console.error( `[carte:${event.correlationId}] ${event.category} on query "${event.queryId}" (panel ${event.panelIndex})`, event.cause, ); }; /** * Thrown by `executePlan` when a panel cannot be executed. The error's public * surface (`category`, `panelIndex`, `queryId`, `correlationId`) is bounded * and safe to surface to clients or feed into model-visible payloads via * `toModelSafeJSON()`. The original `cause` (driver error, ZodError) is * preserved on `Error.prototype.cause` for server-side debugging only — * `toModelSafeJSON()` deliberately excludes it. * * This is Invariant 2 in code form: nothing originating from query execution * (driver throws, returned rows, ZodError `received` values) reaches the * model-visible projection. */ export class CarteExecutionError extends Error { readonly panelIndex: number; readonly queryId: string; readonly category: CarteExecutionErrorCategory; readonly correlationId: string; constructor(options: { category: CarteExecutionErrorCategory; panelIndex: number; queryId: string; cause?: unknown; correlationId?: string; }) { super(messageFor(options.category, options.queryId), options.cause !== undefined ? { cause: options.cause } : undefined); this.name = "CarteExecutionError"; this.category = options.category; this.panelIndex = options.panelIndex; this.queryId = options.queryId; this.correlationId = options.correlationId ?? newCorrelationId(); } /** * Closed-enum projection safe to serialize to clients or feed into * model-visible surfaces. Excludes `cause` and `message` (both can contain * externally-sourced values) by construction. */ toModelSafeJSON(): CarteExecutionErrorPublic { return { category: this.category, panelIndex: this.panelIndex, queryId: this.queryId, correlationId: this.correlationId, }; } } function messageFor(category: CarteExecutionErrorCategory, queryId: string): string { switch (category) { case "unknown_query": return `Unknown query: ${queryId}`; case "invalid_params": return `Invalid params for query "${queryId}"`; case "query_failed": return `Query "${queryId}" execution failed`; case "returns_mismatch": return `Query "${queryId}" returned data that does not match its declared returns schema`; case "context_unauthorized": return "Authentication required"; } } function newCorrelationId(): string { // crypto.randomUUID is available on Node >= 19 and all modern browsers; the // package targets Node >= 22 (see root package.json engines). return crypto.randomUUID(); }