import { type DB } from '../db/client.js'; import type { EscalationRow } from '../db/schema/escalations.js'; export type EscalationUrgency = 'low' | 'medium' | 'high' | 'critical'; export type EscalationReason = 'crisis' | 'ambiguous' | 'policy_violation' | 'patient_requested' | 'tool_failed' | 'cost_limit' | (string & {}); export interface EscalateArgs { /** * E.164 identifier of the user whose turn was escalated. Required for the * default schema; may be omitted when `omitColumns` includes `'whatsapp'` * (the app routes via a different column, e.g. a `patient_id` FK). When * omitted, the in-memory `EscalationRow.whatsapp` field defaults to `''` * for notifier consumers. */ whatsapp?: string | null; reason: EscalationReason; urgency: EscalationUrgency; message: string; traceId?: string | null; tenantId?: string | null; /** * Extra columns INSERTed alongside the framework columns — used when the * app's schema has columns the framework doesn't model (e.g. psico's * `patient_id` FK). Keys must appear in `EscalationStoreOptions.allowedExtraColumns`; * unlisted keys throw at runtime. Values are parameterized — only the * column-name identifier needs the allowlist. */ extraColumns?: Record; } export interface ResolveArgs { resolvedBy?: string | null; notes?: string | null; } export interface ListEscalationsOptions { activeOnly?: boolean; urgency?: EscalationUrgency; tenantId?: string; whatsapp?: string; limit?: number; } export interface EscalationNotifier { notify(escalation: EscalationRow): Promise; } /** * Logical column names on the row shape (`EscalationRow`). Used by * `columnMap` to point each one at a different physical column name when * an app already has its own table. */ export type EscalationField = 'id' | 'whatsapp' | 'reason' | 'urgency' | 'message' | 'traceId' | 'tenantId' | 'createdAt' | 'resolvedAt' | 'resolvedBy' | 'notes'; /** * Default column names — mirror `migrations/013_escalations.sql`. Override * via `EscalationStoreOptions.columnMap` to retarget an app-owned table. */ export declare const DEFAULT_ESCALATION_COLUMNS: Readonly>; export interface EscalationStoreOptions { /** * D1 binding OR a pre-built Drizzle client (any schema). v0.7+: * normalized internally; pass `env.DB` directly or your own * `createDb(env.DB)` — both work. */ db: D1Database | DB; /** Optional sink for newly-recorded escalations. Default: NoOpNotifier. */ notifier?: EscalationNotifier | null; /** * Minimum urgency that triggers the notifier. Below this, only D1 is * touched. Default 'medium' — `low` escalations are recorded silently. */ notifyAtOrAbove?: EscalationUrgency; /** * Physical table name. Default `'escalations'`. Override when the app * already owns the schema and you want the store to write to that. * * Must be a bare SQL identifier (alphanumeric + underscores). Rejected * at construction; this is the same defence-in-depth `ContentGenerator` * uses. */ tableName?: string; /** * Per-field column-name override. Pass only the fields whose physical * column differs from the default; the rest fall through to * `DEFAULT_ESCALATION_COLUMNS`. * * columnMap: { notes: 'resolution' } * // psico's column is `resolution` instead of `notes`. * * Like `tableName`, each value must be a bare SQL identifier. */ columnMap?: Partial>; /** * Logical fields to skip from INSERT entirely. Use when the app's table * has no such column — e.g. psico's `escalations` routes via `patient_id` * and has no `whatsapp` column, so `omitColumns: ['whatsapp']` keeps the * INSERT off that column. Omitted fields still flow into the in-memory * `EscalationRow` for notifier consumers (with empty defaults when not * populated via `extraColumns`). */ omitColumns?: ReadonlyArray; /** * Allowlist of physical column names accepted from `EscalateArgs.extraColumns`. * Required when callers pass `extraColumns` — keys not in this list throw * at runtime. Each entry must be a bare SQL identifier; values flow as * parameterized bindings. * * allowedExtraColumns: ['patient_id'] * // psico tracks the patient FK alongside the framework columns. */ allowedExtraColumns?: ReadonlyArray; } export declare class EscalationStore { readonly db: DB; readonly notifier: EscalationNotifier; readonly notifyAtOrAbove: EscalationUrgency; readonly tableName: string; readonly columns: Readonly>; readonly omitColumns: ReadonlySet; readonly allowedExtraColumns: ReadonlySet; constructor({ db, notifier, notifyAtOrAbove, tableName, columnMap, omitColumns, allowedExtraColumns, }: EscalationStoreOptions); /** * Insert a row, fire the notifier asynchronously, return the new id. * Failure to notify never blocks the record path — notifier errors are * caught + logged so a downstream outage can't break escalation logging. */ record(args: EscalateArgs): Promise; byId(id: string): Promise; resolve(id: string, args?: ResolveArgs): Promise; list({ activeOnly, urgency, tenantId, whatsapp, limit }?: ListEscalationsOptions): Promise; /** Number of currently open escalations, optionally filtered by urgency. */ openCount({ urgency, tenantId }?: { urgency?: EscalationUrgency; tenantId?: string; }): Promise; /** * Build the SELECT projection that aliases physical columns back to the * logical `EscalationRow` field names. Lets every caller treat the row * shape as the same regardless of the column map. */ private selectList; } export declare class NoOpNotifier implements EscalationNotifier { notify(_escalation: EscalationRow): Promise; } export interface HttpNotifierOptions { url: string; headers?: Record; /** Build the POST body. Default: JSON-stringify the row. */ bodyFor?: (row: EscalationRow) => BodyInit; } export declare class HttpNotifier implements EscalationNotifier { readonly url: string; readonly headers: Record; readonly bodyFor: (row: EscalationRow) => BodyInit; constructor({ url, headers, bodyFor }: HttpNotifierOptions); notify(row: EscalationRow): Promise; } export interface SlackNotifierOptions { /** Slack incoming-webhook URL. */ webhookUrl: string; /** Override the rendered message. Default: a short summary with urgency emoji. */ render?: (row: EscalationRow) => { text: string; blocks?: unknown[]; }; } export declare class SlackNotifier implements EscalationNotifier { readonly webhookUrl: string; readonly render: (row: EscalationRow) => { text: string; blocks?: unknown[]; }; constructor({ webhookUrl, render }: SlackNotifierOptions); notify(row: EscalationRow): Promise; } //# sourceMappingURL=escalation_store.d.ts.map